Golang中goroutine未捕获循环变量的问题
Golang中goroutine未捕获循环变量的问题
为什么这段代码会失败(总是打印 3)?
func main() {
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i)
}()
}
time.Sleep(1 * time.Second)
}
而这段代码却能正常工作?
func main() {
for i := 0; i < 3; i++ {
func() {
go fmt.Println(i)
}()
}
time.Sleep(1 * time.Second)
}
更多关于Golang中goroutine未捕获循环变量的问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html
该设置是实验性的(即必须手动启用),因为Go团队对可能破坏现有Go代码的语言变更极为谨慎。
Go团队对可能损害现有Go代码的语言变更极为谨慎,因此该设置是实验性的(即必须手动激活)。
更多关于Golang中goroutine未捕获循环变量的问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
Go团队对于可能破坏现有Go代码的语言变更极其谨慎,因此该设置是实验性的(即必须手动启用)。
Go团队对于可能破坏现有Go代码的语言变更极其谨慎,因此该设置是实验性的(即必须手动启用)。
更多信息 https://eli.thegreenplace.net/2019/go-internals-capturing-loop-variables-in-closures/
你可以使用 go vet 来避免这类问题。
或者使用 go lint(它包含了 go vet)。
jarrodhroberson:
这是预期的行为,任何其他支持闭包类型函数的线程语言都以完全相同的方式工作。
这可能是对的,但即便如此,这种预期行为也仅对那些了解其他支持线程的语言如何工作的人而言是显而易见的。
许多新手会偶然遇到这种行为,并且可能会认为前面提到的 loopvar 实验比当前的行为更符合逻辑。
因为在协程被调度执行时,i 的值已经是 3 了。
如果你想在协程中使用外部变量,请在创建协程时将其作为参数传入:
func main() {
for i := 0; i < 3; i++ {
go func(i int) {
fmt.Println(i)
}(i)
}
time.Sleep(1 * time.Second)
}
Go 1.21 将包含一个实验性设置来改变循环变量的行为。它解决了这个问题,以及 goroutine 上下文之外类似的循环问题,这些问题都以某种方式涉及捕获的循环变量的延迟求值。
该设置是实验性的(即必须手动启用),因为 Go 团队对于可能破坏现有 Go 代码的语言变更极为谨慎。
对于所有给出的答案,我认为我们已经明确了为什么第一种情况不起作用。
我仍然困惑的是(但或许已经在某种程度上得到了解答)为什么第二种情况,即:
func main() {
for i := 0; i < 3; i++ {
func() {
go fmt.Println(i)
}()
}
time.Sleep(1 * time.Second)
}
确实能正常工作。
在这里,我们既没有使用局部作用域变量,也没有在循环使用的匿名函数中使用参数。
这是一个经典的Go语言闭包捕获循环变量问题。第一个代码失败的原因是所有goroutine共享同一个变量i的引用,当goroutine执行时循环已经结束,i的值变成了3。
问题分析:
在第一个示例中:
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i) // 捕获的是外部变量i的引用
}()
}
所有goroutine共享同一个i变量,当goroutine开始执行时,循环已经完成,i的值是3。
正确解决方案:
- 传递参数(推荐):
for i := 0; i < 3; i++ {
go func(x int) {
fmt.Println(x)
}(i) // 将i的值作为参数传递
}
- 创建局部变量副本:
for i := 0; i < 3; i++ {
i := i // 创建局部变量副本
go func() {
fmt.Println(i)
}()
}
关于第二个示例:
第二个示例能正常工作的原因是fmt.Println(i)在闭包外部执行,i的值在创建goroutine时就已经确定了:
func() {
go fmt.Println(i) // i的值在闭包创建时确定
}()
这实际上等价于:
go fmt.Println(0)
go fmt.Println(1)
go fmt.Println(2)
完整示例:
func main() {
// 正确的方式1:传递参数
for i := 0; i < 3; i++ {
go func(x int) {
fmt.Println("方式1:", x)
}(i)
}
// 正确的方式2:创建局部副本
for i := 0; i < 3; i++ {
i := i
go func() {
fmt.Println("方式2:", i)
}()
}
time.Sleep(1 * time.Second)
}
输出结果会是0、1、2的某种排列组合,但不会是3。


