Go的 range loop 循环变量
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,原因是如下:
- 在第20行
go v.print()
,每次循环都复用变量v
,而且field
类型的print()
方法的receiver是一个*field
,所以执行v.print()
首先会获得v
的地址,然后作为receiver的值来调用func (p *field) print()
。 - 每次循环使用的都是同一个变量
v
,所以虽然调用了三次v.print()
,实际上都是对同一个变量v
进行的。v
在range loop中是data
中的元素的拷贝,最后的拷贝是{"three"}
。
修复这个问题的办法在Go代码中有标准做法,就是创建一个新的变量:
上面这个代码就会输出 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()
方法实际上是如下类型的一个函数:
另外,在传递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 国际许可协议进行许可。