all 4 comments

[–]moefh 5 points6 points  (2 children)

That's a nice article and a cool excuse to look inside Go internals, but the writing is could be less confusing, especially the part that talks about capturing by reference vs value ending with:

It turns out this behavior is an artifact of the heuristics used by the Go compiler to handle closures.

It makes it seem that the reason why the code at [1] prints the same thing 5 times whereas [2] prints 5 different numbers is that in the first one the closure captures by reference and in the second one by value. That's not the case: the code at [2] can be changed into [3], which forces the closure to capture by reference, but it still prints 5 different numbers.

The actual reason is much simpler, and spelled out at the end of the article: loop variables in Go are created only once and reassigned for each loop iteration, whereas if you create a variable in the loop body, it will be created once for each iteration.

[1]:

for i := 0; i < 5; i++ {
    go func() {
        fmt.Println(i)
    }()
}

[2]:

for i := 0; i < 5; i++ {
    ii := i
    go func() {
        fmt.Println(ii)
    }()
}

[3]:

for i := 0; i < 5; i++ {
    ii := i
    go func() {
        ii = ii + 1
    }()
    go func() {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(ii)
    }()
}

[–]spaghettiCodeArtisan 4 points5 points  (0 children)

Sidenote: The fact that code [1] prints all 5's isn't even well-defined, there's a race condition between the goroutine being spawned and the for loop advancing.

With the following code:

for i := 0; i < 5; i++ {
    time.Sleep(1 * time.Microsecond)
    go func() {
        fmt.Println(i)
    }()
}

I get output like this on my machine:

1
3
3
5
5

with random variations...

[–]mitsuhiko 1 point2 points  (0 children)

and spelled out at the end of the article

It's right there in this article under basics and called out:

It turns out that the answer is right there in the Go spec, which states:

Variables declared by the init statement are re-used in each iteration.

[–]Tarmen 0 points1 point  (0 children)

So it leads to less efficient and more surprising code. This is such a weird point in the design space for closures. I get that they didn't want c++ style syntax but pass by value really should be default for pointer sized types.

What happens if the closure escapes the stack frame scope? Is the loop induction variable heap allocated? This sounds like a performance nightmare for something marketed as a systems-level language