Why does Go handle closures differently in goroutines?
Asked Answered
A

3

19

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.

Anecdotic answered 18/9, 2014 at 17:48 Comment(4)
I dig @Mitchell's go func(s string) { ... }(s) idiom. Another way of putting the issue is that Go scoping rules mean the funcs in both your examples are accessing whatever the current value of s is when they run; the goroutine just runs at a different time.Endolymph
Running this code with the race-detector should reveal the problem.Botti
go vet can detect this problem, go.dev/doc/faq#closures_and_goroutinesCorroborate
This behavior is no longer observed, due to changes in 1.22+ for loops. See: go.dev/blog/loopvar-previewStationery
S
30

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)
}
Spoonbill answered 18/9, 2014 at 17:55 Comment(3)
Thanks Mitchell. @ericflo also pointed me to the spot in the docs that covers it: golang.org/doc/faq#closures_and_goroutinesAnecdotic
Why is a variable in the closure passed by reference and not by value (as is the case in other goroutines)?Herewith
A for loop actually reuses the same variable multiple times This blew my mind. I had a weird issue today and I was passing the address of the range for variable to a function and thought Go should take care of the enclosure and reference memory. It did, except it uses the "wrong" address. Usually a range for variable is allocated and scoped for each iteration, but golang does not apparently and it is scoped to the whole for loop (which reminds me of ES5)...Carvajal
P
6

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 name x
  • In the first for loop, the func () {} () part got executed in each iteration and the loop only continue to its next iteration after func () {} () 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 variable s is the last element in the slice which is bar. That's why we got two "bar"s in the second for loop output.
Plutus answered 29/8, 2016 at 15:30 Comment(0)
I
1

As of Go v1.22 loop scoping has changed (for the better), see Fixing For Loops in Go 1.22.

A new variable s will be created for each iteration so the goroutines will now print the same value for x and s.

Implied answered 10/6 at 3:33 Comment(1)
Here's a fixed link to the blog: go.dev/blog/loopvar-previewQuartet

© 2022 - 2024 — McMap. All rights reserved.