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

huangapple go评论84阅读模式
英文:

Why does Go handle closures differently in goroutines?

问题

考虑以下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)在goroutine版本中,s的值被解释得与常规函数调用中不同,以及b)为什么在循环内将其赋值给一个局部变量在两种情况下都有效。

英文:

Consider the following Go code (also on the 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)
}

This code produces the following output:

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

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

Assuming this isn't some odd compiler bug, I'm curious why a) the value of s is interpreted differently in the goroutine version then in the regular func call and b) and why assigning it to a local variable inside the loop works in both cases.

答案1

得分: 29

在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)
}
英文:

Closures in Go are lexically scoped. This means that any variables referenced within the closure from the "outer" scope are not a copy but are in fact a reference. A for loop actually reuses the same variable multiple times, so you're introducing a race condition between the read/write of the s variable.

But x is allocating a new variable (with the :=) and copying s, which results in that being the correct result every time.

In general, it is a best practice to pass in any arguments you want so that you don't have references. Example:

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

答案2

得分: 6

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

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

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”的原因。
英文:

Tip:
You can use the "get address operator" <kbd>&</kbd> to confirm whether or not variables are the same.

Let's slightly modify your program to help our understanding.

package main

import &quot;fmt&quot;
import &quot;time&quot;

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

The output is:

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



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

Key points:

  • The variable s in each iteration of the loop is the same variable.
  • The local variable x in each iteration of the loop are different variables, they just happen to have the same name x
  • In the first for loop, the func () {} () part got executed in each iteration and the loop only continue to its next iteration after func () {} () completed.
  • In the second for loop (goroutine version), the go func () {} () statement itself completed instantaneously. When the statements in the func body got executed is determined by the Go scheduler. But when they (the statements in the func body) starts to execute, the for loop already completed! And the variable s is the last element in the slice which is bar. That's why we got two "bar"s in the second for loop output.

huangapple
  • 本文由 发表于 2014年9月19日 01:48:35
  • 转载请务必保留本文链接:https://go.coder-hub.com/25919213.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定