这几天在学习Go语言,一段代码让我感到百思不得其解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"time"
)

type field struct {
name string
}

func (p *field) print() {
fmt.Println(p.name)
}

func main() {
data := []field{ {"one"}, {"two"}, {"three"} }

for _, v := range data {
go v.print()
}
time.Sleep(3 * time.Second)
}

输出:

1
2
3
three
three
three

从表面来看,上面的代码中每循环一次创建一个协程,来打印结构体中的name字段。第一次循环,v的值为{"one"},所以第一个创建的协程应该打印"one",第二个协程应该打印"two",以此类推。但多次运行该程序,输出结果始终只有"three"。这不太符合常理,于是我对代码进行了一些改动,在循环内部,为打印函数添加一个wrapper:

1
2
3
4
5
for _, v := range data {
go func() {
v.print()
}()
}

输出结果还是相同,但此时编译器发出了警告:

1
loop variable v captured by func literal

这句警告是什么意思我也没有搞懂。我想用其他的办法找出这个bug的源头。添加--race选项运行该程序,得到的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
WARNING: DATA RACE
Read at 0x00c00011a210 by goroutine 7:
main.main.func1()
/home/avalanche/dev/go/go-learn/main.go:18 +0x3a

Previous write at 0x00c00011a210 by main goroutine:
main.main()
/home/avalanche/dev/go/go-learn/main.go:16 +0xd3

Goroutine 7 (running) created at:
main.main()
/home/avalanche/dev/go/go-learn/main.go:17 +0x164

确实发生了数据竞争,但内存地址为0x00c00011a210处是哪一个变量无从知晓,猜测是v。但从数据竞争也无法从逻辑上推出程序的运行结果,作罢,求助于Google,找到了Stack Overflow上的一个帖子

高赞给出的解释是这样的:在循环中创建的这些goroutine中,v指向for...range循环中的循环变量v,也就是说,在这些goroutine运行的时候,v的值是for...range循环中的v,而不是这些goroutine创建时v的值。线程main的运行速度很快,在这些goroutine还没有运行起来的时候,循环遍历已经完成,所以在这些goroutine运行时,协程中的v全部指向最后一个循环变量,在这个例子中就是{"three"}。如果想要达到不同的线程输出不同值的效果,就要用传参的方式来创建这些goroutine:

1
2
3
4
5
for _, v := range data {
go func(v field) {
v.print()
}(v)
}

此时程序的输出结果为:

1
2
3
three
one
two

而且多运行几次,每一次运行的结果都有可能不同。