Golang: Language: Internal: Capturing Loop Variables

16th December 2021 at 4:25pm

Basic

func PrintInt(n int) {
      fmt.Println(n)
}

func main() {
      for i := 0; i < 5; i++ {
        	go PrintInt(i)
      }

      time.Sleep(100 * time.Millisecond)
}

上面代码会乱序输出 0 ~ 4。没有意外。

func PrintInt(n *int) {
      fmt.Println(*n)
}

func main() {
      for i := 0; i < 5; i++ {
        	go PrintInt(&i)
  	}

  	time.Sleep(100 * time.Millisecond)
}

上面代码大概率会全部输出 5。如果用 -race 来运行,会报 data race。

原因

From Go spec:

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

即是说在 for 循环的 init statement 部分声明的函数

这意味着 Go 不会 为循环的每一次 iteration 都重新分配一个 i 的内存空间。PrintInt(&i) 的 5 次调用,获得的参数(&i)都是一样的地址。

为什么会有 data race?因为 main 函数的 goroutine 在修改 i,新起的 5 个协程在读取 i

Methods with value vs. pointer receivers

下面的两段代码,区别在于 Show 函数的 receiver 是 value 还是 pointer。这会造成它们输出也不同。想想为什么?

type MyInt int

func (mi MyInt) Show() {
    fmt.Println(mi)
}

func main() {
    ms := []MyInt{50, 60, 70, 80, 90}
    for _, m := range ms {
        go m.Show()
    }

    time.Sleep(100 * time.Millisecond)
}
type MyInt int

func (mi *MyInt) Show() {
    fmt.Println(*mi)
}

func main() {
    ms := []MyInt{50, 60, 70, 80, 90}
    for _, m := range ms {
        go m.Show()
    }

    time.Sleep(100 * time.Millisecond)
}

原因

Method 可以认为是第一个参数是 receiver 的函数。

Closures

闭包在使用 loop variable 时候要特别注意。

func foobyval(n int) {
    fmt.Println(n)
}

func main() {
    for i := 0; i < 5; i++ {
        go func() {
            foobyval(i)
        }()
    }

    time.Sleep(100 * time.Millisecond)
}

这段代码大概率会打印出 5 个 5。原因是:

i is a free variable inside the closure, and such variables are captured by reference in Go.

即是说,closure 中使用了外部的变量时,变量是 pass by reference 的。上面的代码可以翻译成:

func func1(i *int) {
    foobyval(*i)
}

for i := 0; i < 5; i++ {
    go func1(&i)
}

简单的解决办法是:

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

有一种防御性写法,是在循环体的第一行重新定义一个同名变量。代价是额外的拷贝:

for _, item := range items {
    item := item
    // ...
}

参考