不同receiver类型引发的问题 #
代码是这样的:
package main
import (
"fmt"
"time"
)
type field struct {
name string
}
func (p *field) print() {
fmt.Println(p.name)
}
func main() {
data1 := []*field{{"one"}, {"two"}, {"three"}}
for _, v := range data1 {
go v.print()
}
data2 := []field{{"four"}, {"five"}, {"six"}}
for _, v := range data2 {
go v.print()
}
time.Sleep(3 * time.Second)
}
运行结果是这样(由于 Goroutine 调度顺序不同,你自己的运行结果中的行序可能与下面的有差异):
one
two
three
six
six
six
为什么对 data2 迭代输出的结果是三个“six”,而不是 four、five、six?
Go 方法的本质是一个以方法的 receiver 参数作为第一个参数的普通函数,对这个程序做个等价变换。利用 Method Expression 方式,等价变换后的源码如下:
type field struct {
name string
}
func (p *field) print() {
fmt.Println(p.name)
}
func main() {
data1 := []*field{{"one"}, {"two"}, {"three"}}
for _, v := range data1 {
go (*field).print(v)
}
data2 := []field{{"four"}, {"five"}, {"six"}}
for _, v := range data2 {
go (*field).print(&v)
}
time.Sleep(3 * time.Second)
}
我们可以很清楚地看到使用 go 关键字启动一个新 Goroutine 时,method expression 形式的 print 函数是如何绑定参数的:
- 迭代 data1 时,由于 data1 中的元素类型是 field 指针 (*field),因此赋值后 v 就是元素地址,与 print 的 receiver 参数类型相同,每次调用 (*field).print 函数时直接传入的 v 即可,实际上传入的也是各个 field 元素的地址;
- 迭代 data2 时,由于 data2 中的元素类型是 field(非指针),与 print 的 receiver 参数类型不同,因此需要将其取地址后再传入 (*field).print 函数。这样每次传入的 &v 实际上是变量 v 的地址,而不是切片 data2 中各元素的地址。
由于 参与 for range 循环的是 range 表达式的副本,这里的 v 在整个 for range 过程中只有一个,因此 data2 迭代完成之后,v 是元素“six”的拷贝。
这样,一旦启动的各个子 goroutine 在 main goroutine 执行到 Sleep 时才被调度执行,那么最后的三个 goroutine 在打印 &v 时,实际打印的也就是在 v 中存放的值“six”。而前三个子 goroutine 各自传入的是元素“one”、“two”和“three”的地址,所以打印的就是“one”、“two”和“three”了。
那么原程序要如何修改,才能让它按我们期望,输出“one”、“two”、“three”、“four”、 “five”、“six”呢?
其实,我们只需要将 field 类型 print 方法的 receiver 类型由 *field 改为 field 就可以了。我们直接来看一下修改后的代码:
type field struct {
name string
}
func (p field) print() {
fmt.Println(p.name)
}
func main() {
data1 := []*field{{"one"}, {"two"}, {"three"}}
for _, v := range data1 {
go v.print()
}
data2 := []field{{"four"}, {"five"}, {"six"}}
for _, v := range data2 {
go v.print()
}
time.Sleep(3 * time.Second)
}
修改后的程序的输出结果是这样的:
one
two
three
four
five
six