英文:
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 "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)
}
The output is:
&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
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 namex
- In the first for loop, the
func () {} ()
part got executed in each iteration and the loop only continue to its next iteration afterfunc () {} ()
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 variables
is the last element in the slice which isbar
. That's why we got two "bar"s in the second for loop output.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论