loopsgoscope

Go Ranged Loop Scope and Indirect Function


Can someone help enlighten me with the "why" aspect of the following surprise I had with golang loop scoping and temporary functions? Following is an excerpt reduction from some more-complex code:

package main

import ( "fmt" )

type Caller struct {
    call func()
}

func printer(val int) {
    fmt.Printf("the value is %v\n", val)
}

func main () {
    values := []int{1,2,3}

    var callers []Caller
    for _,val := range values {
        call := func() { printer(val) }
        callers = append(callers, Caller{call})
    }

    for _, caller := range callers {
        caller.call()
    }
}

which produces the (to me) surprising outcome:

the value is 3
the value is 3
the value is 3

If I alter that code by changing the range values loop body to this:

        theVal := val
        call := func() { printer(theVal) }
        callers = append(callers, Caller{call})

then we get the originally hoped-for result:

the value is 1
the value is 2
the value is 3

Fundamentally, one works and the other doesn't - I'm clear on that and can try to just memorize the idiom. I'm hoping for more understanding, perhaps a little life lesson in golang. What is it about scoping rules and deferred execution that means the loop variable maintains the final valid value and is submitted into every one of the temporary functions built during the loop? Why isn't the value "val" dropped into the dynamically-built func "call"? I suspect that I am confused about something fundamental. Even seeing a working version, I am not certain I will be able to avoid a trap like this in the future. If you have advice for "why" the iterated value acts like that, I'd love to hear it (and thank you in advance).


Solution

  • This will work for you as well. Here's the link from the FAQ.

    package main
    
    import (
        "fmt"
    )
    
    type Caller struct {
        call func()
    }
    
    func printer(val int) {
        fmt.Printf("the value is %v\n", val)
    }
    
    func main() {
        values := []int{1, 2, 3}
    
        var callers []Caller
        for _, val := range values {
            var call func()
            func(v int) {
                call = func() {
                    printer(v)
                }
            }(val)
            callers = append(callers, Caller{call})
        }
        for _, caller := range callers {
            caller.call()
        }
    }
    

    An alternate method would be to bind the current value of val to each closure as it is launched, you can store it in a new variable and then use it (the method you have done to solve it).