为什么 Go 在 goroutine 中以不同的方式处理闭包?

问题描述 投票:0回答:3

考虑以下 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)以及为什么将其分配给循环内的局部变量有效在这两种情况下。

go closures
3个回答
30
投票

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

6
投票

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

让我们稍微修改一下您的程序以帮助我们理解。

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 循环输出中得到两个“条”。

0
投票

从 Go v1.22 开始,循环范围已更改(变得更好),请参阅修复 Go 1.22 中的 For 循环

将为每次迭代创建一个新变量

s
,因此 goroutine 现在将为
x
s
打印相同的值。

© www.soinside.com 2019 - 2024. All rights reserved.