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

9 回复

该设置是实验性的(即必须手动启用),因为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)
}

确实能正常工作。

在这里,我们既没有使用局部作用域变量,也没有在循环使用的匿名函数中使用参数。

这是预期的行为,任何其他支持线程且能使用闭包类型函数的语言都以完全相同的方式工作。

您需要在闭包内部捕获状态。最好且最不易出错的方式是使用函数参数。另一种方式则较为微妙,即使是经验丰富的开发者也可能忽略,即通过将局部作用域中的变量赋值为函数实例创建时的值。大多数情况下,您会看到以最糟糕的方式实现这一点:变量遮蔽。在您的情况下,它看起来会像这样。“聪明”的程序员使用“简写”隐式版本,这会让新手甚至他们自己在后续阅读代码时感到困惑。

func() {
    i := i
    go fmt.Println(i)
}()

显式的函数参数是传达所需语义的最佳方式。

func(i int) {
	go fmt.Println(i)
}(i)

这是一个经典的Go语言闭包捕获循环变量问题。第一个代码失败的原因是所有goroutine共享同一个变量i的引用,当goroutine执行时循环已经结束,i的值变成了3。

问题分析:

在第一个示例中:

for i := 0; i < 3; i++ {
    go func() {
        fmt.Println(i)  // 捕获的是外部变量i的引用
    }()
}

所有goroutine共享同一个i变量,当goroutine开始执行时,循环已经完成,i的值是3。

正确解决方案:

  1. 传递参数(推荐)
for i := 0; i < 3; i++ {
    go func(x int) {
        fmt.Println(x)
    }(i)  // 将i的值作为参数传递
}
  1. 创建局部变量副本
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。

回到顶部