golang 循环中的闭包问题

update 解释:

首先原始问题涉及到了两个golang的特性, 下面是一段 spec 里面关于go statments的解释:

The function value and parameters are evaluated as usual in the calling goroutine, but unlike with a regular call, program execution does not wait for the invoked function to complete.

https://golang.org/ref/spec#Go_statements

根据上面的解释,当传递给go 关键字的是一个 expression 是, golang首先会 evaluate 这个expression,也就是说 go v.print() 中的v在go routine启动之前就已经传入了 print 中, 所以对于 TestClosure和TestClosure1中的v都是这种情况,但为什么两个出现的结果不同呢?

这里就要说到第二个特性就是:

The method set of any other type T consists of all methods declared with receiver type T. The method set of the corresponding pointer type *T is the set of all methods declared with receiver *T or T (that is, it also contains the method set of T)

https://golang.org/ref/spec#Method_sets

因为print需要一个 *field 作为接受者,所以如果直接在一个 field的 instance 上调用print方法,我们使用 v.print() 实际上golang发现 print方法需要的是一个 pointer receiver的时候,就会把这个调用实际写作 (&v).print(), 所以在TestClosure中 v 就是 *field, 所以不需要再去 (&v).print() 这样来调用,而在 TestClosure1中 v 是 field, 所以需要 (&v).print() 这样来调用。

第三个特性是:

上面的range方式实际上就是一个简单的loop 可以写作如下:

var v field
for i := 0; i < len(data); i++ {
    v = data[i]
    go func(p *field) {  // 因为 print方法定义在 *field 上, 所以这里需要的就是 *field
          p.print()
    }(&v)
}

由于 v 在每次循环中都复用,所以传入每个goroutine的地址其实都是一样的,所以对于

go func(){
    p.print()
}()

这种写法而言,即使是 data := []*field{} , 传入goroutine中的地址然仍是最后一个元素的地址,不同于 go v.print() 在go routine启动之前就会 evaluate v.print(), 从而把正确的指针传递给 print函数。

https://golang.org/doc/faq#closures_and_goroutines

感谢: @changwenlong @silenceshell @uguangtian

原始问题

这里有段代码:https://play.golang.org/p/cKrC0oy7FP

type field struct {
    name string
}

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

func TestClosure() {

    data := []*field{{"one"}, {"two"}, {"three"}}

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

    time.Sleep(3 * time.Second)
}

func TestClosure1() {

    data := []field{{"one"}, {"two"}, {"three"}}

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

    time.Sleep(3 * time.Second)
}

这两个TestClosure的唯一差别在于data中, 第一个是 *field 后一个是field

执行的结果是 TestClosure 是 one,two,three TestClosure 是 three,three,three

golang对于循环的变量引用是重复使用 v 这个变量的,实际的地址在两个for循环里都是一样的地址,为什么第一个TestClosure中即使v变量是一样的,但等到实际调用print method后对应的v地址在第一个for循环中又发生了变化 从而产生了正确的结果呢?

如果把 go routine 的启动方式变成

go func(){
    v.print()
}()

那么两个方法是一模一样的 大家有啥想法呢

已邀请:

silenceshell - ieevee.com

赞同来自: AlexaMa uguangtian

尝试答一下。

The method set of the corresponding pointer type T is the set of all methods with receiver T or T (that is, it also contains the method set of T)

也就是说print函数对于TestClosure1里的go v.print()会当做go (&v).print()

TestClosure循环中,传给print函数的值,是data数组里3个成员各自的地址(v是指针,值是变化的),用%p打印:

0xc82000a300 0xc82000a310 0xc82000a320

所以等到print函数去打印的时候,可以取到正确的值;

但TestClosure1循环中传给print函数的值,实际是v的地址指针(v是struct,每次循环都是一次值拷贝,但v的地址不变),因此3个平print函数看到的p值是一样的,都是v的地址;等到print函数去打印的时候,此时去取值,完全看v当前值拷贝的结果是什么。 在你的例子里,for循环此时已经结束,v的值就是最后一次值拷贝的结果({three})。 如果你在循环里加一个time.Sleep(),给goroutine足够的启动时间,看到的结果跟TestClosure是一样的。

chanehua

赞同来自:

go func(v string){ v.print() }(v)

因为你才用goroutine,所以可能for循环已经结束了,但你的goroutine还没获取资源去执行导致的。

uguangtian

赞同来自:

折腾了好一下,我也总算理清楚了,就是如@silenceshell 和@chanehua所说的,主要是指针传递,值传递和Goroutine执行顺序间的问题。

func TestClosure() {
    data := []*field{{"one"}, {"two"}, {"three"}}
    for _, v := range data {
        go v.print()
    }
    time.Sleep(3 * time.Second)
}

这个版本能按序输出。循环中的v只定义了一次,赋值了三次,因为data是指针数组,所以range中传给v的是三个元素的指针,v是指针类型,值各不相同,而在传给Goroutine时,v值已经确定了,所以每次for循环时,go v.print()都把当前循环的值都传去了。


func TestClosure() {
    data := []*field{{"one"}, {"two"}, {"three"}}
    for _, v := range data {
        go func(){
        v.print()
        }()
    }
    time.Sleep(3 * time.Second)
}

这一版本中只输出了最后一个元素,因为在Goroutine下执行的顺序无法预估。这段中for都执行完了go func(){}才执行,等到v.print()执行的时候,v变量已经被迭代到最后一个元素了。下面的这段可以验证这点。

func TestClosure() {
    data := []*field{{"one"}, {"two"}, {"three"}}
    for _, v := range data {
        fmt.Println("for")
        go func() {
            fmt.Println("go func")
            v.print()
        }()
    }
    time.Sleep(3 * time.Second)
}

hechen

赞同来自:

@silenceshell 我感觉还是不能完全解释

如果说TestClosure1循环中传给print函数的值是v的地址指针,那么在TestClosure循环中传的就不是了么? 那为什么在TestClosure中的print就可以拿到正确的每一个元素的指针,而TestClosure1中的print就不能拿到正确的元素的值呢?

TestClosure1中拿不到正确值的原因是 go routine在启动之前 for循环就结束了, 正如你所说的在循环中加一个time.Sleep(), 所以说在TestClosure1中的go routine拿到的都是最后一个v的拷贝,也就是最后一个元素的值对应的指针。那为什么这个逻辑在TestClosure的循环中就不存在呢? 为什么TestClosure中的print函数拿到的就是正确的每个元素的指针,而不是最后一个元素的指针呢?

而且如果你的解释是成立的话,把TestClosure中的 go routine启动方式改成了:

go func(){
    v.print()
}()

结果应该会不变才对, 那为什么实际的结果就是使用了这种启动方式之后,TestClosure就变得和TestClosure1一样了呢?

hechen

赞同来自:

@uguangtian

你的意思是

go v.printf()
go func(){
    v.printf()
}()

这两种go routine启动方式不同在于 在第一种启动方式时,每次循环v都会被传递到printf() ? 而第二种则不会么? 为啥?

如果第一种启动方式每次都会传递正确的v值给print函数,那为什么只会传递指针,而在TestClosure1中的值就不会每次循环都传递给print函数呢?

changwenlong

赞同来自:

换一种写法:


func TestClosure() {
    fields := []*field{{"one"}, {"two"}, {"three"}}
    for _, v := range fields {
        go func(ff *field) {
            ff.print()
        }(v)
    }
}

func TestClosure1() {
    fields := []field{{"one"}, {"two"}, {"three"}}
    for _, v := range fields {
        go func(ff *field) {
            ff.print()
        }(&v)
    }
}

要回复问题请先登录注册