越过山丘
遇见六十岁的我
拄着一根白手杖
在听鸟儿歌唱
我问他幸福与否
他笑着摆了摆手
在他身边围绕着一群
当年流放归来的朋友
他说你不必挽留
爱是一个人的等候
等到房顶开出了花
这里就是天下
总有人幸福白头
总有人哭着分手
无论相遇还是不相遇
都是献给岁月的序曲
🎵 杨宗纬《越过山丘》
在 Go 语言中,一个常见的变量覆盖案例涉及到闭包和并发。当你在一个循环中启动多个并发的 goroutine,并且这些 goroutine 引用了循环的迭代变量时,就有可能发生覆盖。这是因为闭包中的变量是通过引用捕获
的,而不是通过值。如果闭包在下一次迭代之前没有执行,那么它使用的变量值可能就是迭代变量的最新值。
下面是一个简单的示例,说明了如何在 Go 语言中发生这种覆盖:
package mainimport ("fmt""sync""time"
)func main() {var wg sync.WaitGroupfor i := 0; i < 5; i++ {wg.Add(1)go func() {fmt.Println(i) // 此处的 i 可能在 goroutine 执行时被覆盖wg.Done()}()}// 给 goroutines 时间启动time.Sleep(time.Second)wg.Wait()
}
在上面的代码中,我们启动了 5 个 goroutine,每个都打印变量 i 的值。但是,因为这些 goroutine 可能在 for 循环结束后才开始执行,所以它们都可能打印出同一个数字(通常是最后一个迭代的数字,即 4),而不是每个 goroutine 打印出其对应迭代的数字。
在 Go 中,goroutine 是并发执行的,这意味着它们是在程序的其他部分独立运行的轻量级线程。当你在 for 循环中使用 go 关键字启动一个 goroutine 时,Go 会计划在未来的某个时间点运行这个 goroutine。这个确切的时间点是由 Go 运行时的调度器决定的,它处理所有的并发任务并决定它们的执行顺序。
由于 goroutine 的启动是非阻塞的,for 循环并不会等待每个 goroutine 启动或完成。循环会立即继续执行,进入下一次迭代,最终在所有 goroutine 都被计划后很快完成。
因此,如果 goroutine 内部使用了循环变量,例如上面例子中的 i,并且 goroutine 的执行被推迟到循环完成之后,所有的 goroutine 可能会看到 i 的最终值,因为它们都引用
了同一个变量 i。
在实践中,这意味着在循环完成之前,goroutine 可能没有机会开始执行。特别是在循环迅速执行并且没有显式的同步机制(如 sync.WaitGroup)来等待每个 goroutine 的完成的情况下,很可能所有 goroutine 都将在循环结束后才开始执行。
在处理 goroutine 和循环变量时,最佳实践是将每次迭代的变量值传递给 goroutine,以避免意外捕获循环变量的最终值。这可以通过将迭代变量作为参数传递给 goroutine 的匿名函数来实现,从而为每个 goroutine 创建一个变量的副本。这样,即使 goroutine 在循环结束后开始执行,它也将具有正确的迭代值。
为了避免这个问题,你可以在每次迭代中创建一个循环作用域内的变量副本,并将其传递给闭包:
package mainimport ("fmt""sync""time"
)func main() {var wg sync.WaitGroupfor i := 0; i < 5; i++ {wg.Add(1)go func(localI int) { // 使用局部变量 localIfmt.Println(localI) // 打印局部变量,而不是循环变量 iwg.Done()}(i) // 传递当前迭代的 i 的值}// 给 goroutines 时间启动time.Sleep(time.Second)wg.Wait()
}
这个修改后的版本为每个迭代创建了一个 localI
变量的副本,并将其传递给每个 goroutine。这样,每个 goroutine 有它自己的 localI 副本,而且不会被其他迭代覆盖。