不同receiver类型引发的问题

不同receiver类型引发的问题

不同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 函数是如何绑定参数的:

  1. 迭代 data1 时,由于 data1 中的元素类型是 field 指针 (*field),因此赋值后 v 就是元素地址,与 print 的 receiver 参数类型相同,每次调用 (*field).print 函数时直接传入的 v 即可,实际上传入的也是各个 field 元素的地址;
  2. 迭代 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

Viewpoint #

From #

24|方法:理解“方法”的本质