Why does Go handle closures differently in goroutines?
Consider the following Go code (also on the Go Playground):
package main
import "fmt"
import "time"
func main() {
for _, s := range []string{"foo", "bar"} {
x := s
func() {
fmt.Printf("s: %s\n", s)
fmt.Printf("x: %s\n", x)
}()
}
fmt.Println()
for _, s := range []string{"foo", "bar"} {
x := s
go func() {
fmt.Printf("s: %s\n", s)
fmt.Printf("x: %s\n", x)
}()
}
time.Sleep(time.Second)
}
This code produces the following output:
s: foo
x: foo
s: bar
x: bar
s: bar
x: foo
s: bar
x: bar
Assuming this isn't some odd compiler bug, I'm curious why a) the value of s is interpreted differently in the goroutine version then in the regular func call and b) and why assigning it to a local variable inside the loop works in both cases.
Solution 1:
Closures in Go are lexically scoped. This means that any variables referenced within the closure from the "outer" scope are not a copy but are in fact a reference. A for
loop actually reuses the same variable multiple times, so you're introducing a race condition between the read/write of the s
variable.
But x
is allocating a new variable (with the :=
) and copying s
, which results in that being the correct result every time.
In general, it is a best practice to pass in any arguments you want so that you don't have references. Example:
for _, s := range []string{"foo", "bar"} {
x := s
go func(s string) {
fmt.Printf("s: %s\n", s)
fmt.Printf("x: %s\n", x)
}(s)
}
Solution 2:
Tip: You can use the "get address operator" & to confirm whether or not variables are the same.
Let's slightly modify your program to help our understanding.
package main
import "fmt"
import "time"
func main() {
for _, s := range []string{"foo", "bar"} {
x := s
fmt.Println(" &s =", &s, "\t&x =", &x)
func() {
fmt.Println("-", "&s =", &s, "\t&x =", &x)
fmt.Println("s =", s, ", x =", x)
}()
}
fmt.Println("\n\n")
for _, s := range []string{"foo", "bar"} {
x := s
fmt.Println(" &s =", &s, "\t&x =", &x)
go func() {
fmt.Println("-", "&s =", &s, "\t&x =", &x)
fmt.Println("s =", s, ", x =", x)
}()
}
time.Sleep(time.Second)
}
The output is:
&s = 0x1040a120 &x = 0x1040a128
- &s = 0x1040a120 &x = 0x1040a128
s = foo , x = foo
&s = 0x1040a120 &x = 0x1040a180
- &s = 0x1040a120 &x = 0x1040a180
s = bar , x = bar
&s = 0x1040a1d8 &x = 0x1040a1e0
&s = 0x1040a1d8 &x = 0x1040a1f8
- &s = 0x1040a1d8 &x = 0x1040a1e0
s = bar , x = foo
- &s = 0x1040a1d8 &x = 0x1040a1f8
s = bar , x = bar
Key points:
- The variable
s
in each iteration of the loop is the same variable. - The local variable
x
in each iteration of the loop are different variables, they just happen to have the same namex
- In the first for loop, the
func () {} ()
part got executed in each iteration and the loop only continue to its next iteration afterfunc () {} ()
completed. - In the second for loop (goroutine version), the
go func () {} ()
statement itself completed instantaneously. When the statements in the func body got executed is determined by the Go scheduler. But when they (the statements in the func body) starts to execute, the for loop already completed! And the variables
is the last element in the slice which isbar
. That's why we got two "bar"s in the second for loop output.