50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs 中有一个关于循环变量的例子,这个例子提出了一个很有意思的问题。

我们知道Go在 for range 循环中使用同一个变量来存放循环的数据,所以在 range loop中使用goroutine时,需要注意是否引用了同一个变量。当loop range的元素是一个struct类型时,会变得更复杂。

例如下面这个例子(来源):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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)
	//goroutines print: three, three, three
}

上面这个例子会输出三个 three,原因是如下:

  1. 在第20行 go v.print(),每次循环都复用变量v,而且field类型的print()方法的receiver是一个*field,所以执行v.print()首先会获得v的地址,然后作为receiver的值来调用func (p *field) print()
  2. 每次循环使用的都是同一个变量v,所以虽然调用了三次v.print(),实际上都是对同一个变量v进行的。v在range loop中是data中的元素的拷贝,最后的拷贝是{"three"}

修复这个问题的办法在Go代码中有标准做法,就是创建一个新的变量:

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

	for _, v := range data {
		vcopy := v
		go vcopy.print()
	}

	time.Sleep(3 * time.Second)
	//goroutines print: three, three, three
}

上面这个代码就会输出 one, two, three

下面来看一个更复杂的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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)
	//goroutines print: one, two, three
}

这个例子的输出是 one, two, three。和上个例子相比,只有第17行的data定义发生了变化,从直接存储field类型,变成了存储指向field类型的指针。为什么这个变化会使得得到的结果发生改变呢?

首先,要先理解Go的方法的receiver的实际含义。在Go中,一个方法的receiver是实现为这个函数的第一个参数,所以在上面的*field类型的print()方法实际上是如下类型的一个函数:

func fieldPrint(f *field)

另外,在传递receiver时,如果变量不是一个指针,而方法的receiver要求得到一个指针时,Go会先对变量取地址,然后再将这个地址作为receiver来调用方法。

在第一个例子中,代码实际上是这样工作的:

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

import (
	"fmt"
	"time"
)

type field struct {
	name string
}

// 用这个函数替代原来的v.print()方法
func fieldPrint(f *field) {
	fmt.Println(f.name)
}

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

	for _, v := range data {
		// 这里Go自动对v取地址,因为参数是一个指针,参数传递给fieldPrint时是传值调用。
		// 所以三次调用实际上传递的是同一个指针。
		go fieldPrint(&v)
	}

	time.Sleep(3 * time.Second)
	//goroutines print: three, three, three
}

在第二个例子中,代码上是这样工作的:

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

import (
	"fmt"
	"time"
)

type field struct {
	name string
}

// 用这个函数替代原来的v.print()方法
func fieldPrint(f *field) {
	fmt.Println(f.name)
}

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

	for _, v := range data {
		// 这里不用对v取地址,因为v原来就是一个指针,参数传递给fieldPrint时是传值调用。
		// 所以三次调用传递了不同的指针。
		go fieldPrint(v)
	}

	time.Sleep(3 * time.Second)
	//goroutines print: three, three, three
}

知识共享许可协议本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。