【问题标题】:Why does Go handle closures differently in goroutines?为什么 Go 在 goroutine 中处理闭包的方式不同?
【发布时间】:2014-09-18 17:48:35
【问题描述】:

考虑以下 Go 代码(也在 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) 的解释不同,以及为什么将它分配给内部的局部变量循环在这两种情况下都有效。

【问题讨论】:

  • 我挖掘@Mitchell 的go func(s string) { ... }(s) 成语。提出问题的另一种方式是,Go 范围规则意味着 both 中的 funcs 您的示例正在访问它们运行时 s 的当前值; goroutine 只是在不同的时间运行。
  • 使用竞态检测器运行此代码应该会发现问题。

标签: go closures


【解决方案1】:

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

【讨论】:

  • 谢谢米切尔。 @ericflo 还向我指出了涵盖它的文档中的位置:golang.org/doc/faq#closures_and_goroutines
  • 为什么闭包中的变量是通过引用而不是通过值传递的(就像其他 goroutine 中的情况一样)?
  • 一个 for 循环实际上多次重复使用同一个变量 这让我大吃一惊。今天我遇到了一个奇怪的问题,我正在将变量范围的地址传递给一个函数,并认为 Go 应该处理外壳和引用内存。确实如此,只是它使用了“错误”的地址。通常为每次迭代分配一个变量范围并确定其范围,但 golang 显然没有,它的范围是整个 for 循环(这让我想起了 ES5)...
【解决方案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 调度程序确定。但是当它们(函数体中的语句)开始执行时,for 循环已经完成!变量s 是切片中的最后一个元素bar。这就是为什么我们在第二个 for 循环输出中有两个“条”。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-04-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-22
    • 2020-03-11
    • 2018-03-21
    相关资源
    最近更新 更多