goclosures

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

  • 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)
    }