为什么Golang在goroutines中处理不同的闭包?

时间:2014-09-18 17:48:36

标签: go closures

考虑以下Golang代码(也在Go Playground上):

package main

import "fmt"
import "time"

func main() {
    for _, s := range []string{"foo", "bar"} {
        x := s
        func() {
            fmt.Printf("s: %s\n", s)
            fmt.Printf("x: %s\n", x)
        }()
    }
    fmt.Println()
    for _, s := range []string{"foo", "bar"} {
        x := s
        go func() {
            fmt.Printf("s: %s\n", s)
            fmt.Printf("x: %s\n", x)
        }()
    }
    time.Sleep(time.Second)
}

此代码生成以下输出:

s: foo
x: foo
s: bar
x: bar

s: bar
x: foo
s: bar
x: bar

假设这不是一些奇怪的编译器错误,我很好奇为什么a)s的值在goroutine版本中然后在常规func调用和b)中被不同地解释,以及为什么将它分配给内部的局部变量循环适用于两种情况。

2 个答案:

答案 0 :(得分:10)

Go中的闭包是词法范围。这意味着在"外部"范围不是副本,但实际上是参考。 for循环实际上多次重复使用同一个变量,因此您在s变量的读/写之间引入了竞争条件。

x正在分配一个新变量(使用:=)并复制s,这样每次都会得到正确的结果。

一般情况下,传递您想要的任何参数是最佳做法,这样您就没有参考。例如:

for _, s := range []string{"foo", "bar"} {
    x := s
    go func(s string) {
        fmt.Printf("s: %s\n", s)
        fmt.Printf("x: %s\n", x)
    }(s)
}

答案 1 :(得分:2)

提示: 您可以使用"获取地址运算符" & 确认变量是否相同

让我们稍微修改您的计划,以帮助我们理解。

package main

import "fmt"
import "time"

func main() {
    for _, s := range []string{"foo", "bar"} {
        x := s
        fmt.Println("  &s =", &s, "\t&x =", &x)
        func() {
            fmt.Println("-", "&s =", &s, "\t&x =", &x)
            fmt.Println("s =", s, ", x =", x)
        }()
    }

    fmt.Println("\n\n")

    for _, s := range []string{"foo", "bar"} {
        x := s
        fmt.Println("  &s =", &s, "\t&x =", &x)
        go func() {
            fmt.Println("-", "&s =", &s, "\t&x =", &x)
            fmt.Println("s =", s, ", x =", x)
        }()
    }
    time.Sleep(time.Second)
}

输出结果为:

  &s = 0x1040a120   &x = 0x1040a128
- &s = 0x1040a120   &x = 0x1040a128
s = foo , x = foo
  &s = 0x1040a120   &x = 0x1040a180
- &s = 0x1040a120   &x = 0x1040a180
s = bar , x = bar



  &s = 0x1040a1d8   &x = 0x1040a1e0
  &s = 0x1040a1d8   &x = 0x1040a1f8
- &s = 0x1040a1d8   &x = 0x1040a1e0
s = bar , x = foo
- &s = 0x1040a1d8   &x = 0x1040a1f8
s = bar , x = bar

关键点:

  • 循环的每次迭代中的变量s都是相同的变量。
  • 循环的每次迭代中的局部变量x是不同的变量,它们碰巧具有相同的名称x
  • 在第一个for循环中,func () {} ()部分在每次迭代中执行,循环仅在func () {} ()完成后继续下一次迭代。
  • 在第二个for循环(goroutine版本)中,go func () {} ()语句本身即时完成。当执行func主体中的语句时,由Go调度程序确定。但是当它们(func体中的语句)开始执行时,for循环已经完成了!变量s是切片中的最后一个元素bar。这就是为什么我们在第二个for循环输出中得到两个" bar" s。