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 main() {
for i := 0; i < 5; i++ {
go func() {
fmt.Println(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 的。上面的代码可以翻译成:
for i := 0; i < 5; i++ {
go func() {
fmt.Println(*(&i))
}()
}
由于 for
的 init statement,也就是 i := 0
,其中定义的变量是复用同个变量的,意味着 i
在循环的 5 次迭代中,其地址都是不变的。所以 5 个新起的协程在运行时,fmt.Println
所使用的 &i
的地址,与 for
循环中的 i
是同个地址。而这个地址的值在 main 协程中(大概率)已经被修改。因此输出的结果不如预期。
简单的解决办法是:
for i := 0; i < 5; i++ {
go func(i int) {
fmt.Println(i)
}(i)
}
或者在循环体的第一行重新定义一个同名变量:
for i := 0; i < 5; i++ {
i := i
go func() {
fmt.Println(i)
}()
}
它们的代价是接近的,都有一次额外的变量生成和拷贝。