新手问题 golang 循环中的闭包问题

hechen · 2017年06月12日 · 最后由 changwenlong 回复于 2017年06月13日 · 83 次阅读

## 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()
}()

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

更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio

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

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

尝试答一下。

> 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 是一样的。

折腾了好一下,我也总算理清楚了,就是如@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)
}

@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 一样了呢?

@uguangtian

你的意思是

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

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

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

换一种写法:


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)
    }
}

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册