Search code examples
go

Some doubts about golang's closures


type Hello func() int

func sliceCounter1() []Hello {
    hello := make([]Hello, 0, 8)
    for i := 0; i < 5; i++ {
        hello = append(hello, func() int {
            fmt.Println(i)
            return i
        })
    }
    return hello
}

func sliceCounter2() []Hello {
    hello := make([]Hello, 0, 8)
    i := 0
    for i < 5 {
        hello = append(hello, func() int {
            fmt.Println(i)
            return i
        })
        i++
    }
    return hello
}

func main() {
    sc1 := sliceCounter1()
    sc2 := sliceCounter2()

    for _, f1 := range sc1 {
        f1()
    }

    fmt.Println("===========")

    for _, f2 := range sc2 {
        f2()
    }
}

why the result is

0
1
2
3
4
===========
5
5
5
5
5

Why when I debug the code, f1() outputs 5 5 5 5 5, but when I go run the code, f1() outputs 0 1 2 3 4?

I can understand that f2() refers to the address of i, so when it is executed, it outputs the current value of i. But why are the results of normal operation and debugging of f1() inconsistent?


Solution

  • As @Volker mentioned, in sliceCounter1 you'll get a fresh variable for every loop execution since Go 1.22, so all the captured is are different.

    This fixed a number a common bugs where people tried to capture the loop counter at it's current value, so the previous behavior - although correct - was pretty surprising. See the article for a longer explanation.

    In sliceCounter2 you declare one single i and capture that five times. At the time you decide to print it, its value is five.

    For example

    func main() {
        i := 1
    
        printI := func() {
            fmt.Println(i)
        }
    
        incI := func() {
            i++
        }
    
        incI()
        incI()
    
        printI()
    
        incI()
    
        printI()
    }
    

    Prints

    3
    4
    

    as expected.


    Edit I've mentioned the changed behavior since Go 1.22, but as @iBug correctly remarks in Go 1.21 or earlier, you get only one variable in sliceCounter1 and it prints all fives, like sliceCounter2 does. Usually, people don't expect this and the Go team considered this as an empirically non-breaking change.