我们知道,在 Go 语言的for循环中,由于变量作用域问题,可能会导致预期之外的结果,这一问题在 Go 1.22 版本中得到了修复。
来看下面这段代码:
package main
import "sync"
import "time"
func main() {
wg := sync.WaitGroup{}
values := []int{1, 2, 3}
for _, v := range values {
go func() {
fmt.Println(v)
}()
}
<-time.Tick(time.Second)
}
你可能期望输出结果是 1、2、3 各一次,顺序随机。然而,实际结果很可能是 3, 3, 3。这是因为在 for range 循环中,变量 v 的作用域覆盖了整个循环体,每次循环只是更新了 v 的值,而没有创建新的变量。
要避免这个问题,可以在匿名函数中传递 v 的副本,修改后的代码如下:
package main
import "sync"
import "time"
func main() {
wg := sync.WaitGroup{}
values := []int{1, 2, 3}
for _, v := range values {
go func(v int) {
fmt.Println(v)
}(v)
}
<-time.Tick(time.Second)
}
这个问题曾在实际生产环境中导致不少问题。即使知道这个原理,很多开发者仍可能忽视它,反复踩坑。然而,这个问题在 2024 年的 Go 1.22 版本中已经得到了修复。1.22 及之后的版本中,循环时的变量作用域被修改为每次循环都创建一个独立的变量,因此不会再出现上述问题。
你可以理解为新版本中每次循环增加了 v :=v 的操作:
package main
import "sync"
import "time"
func main() {
wg := sync.WaitGroup{}
values := []int{1, 2, 3}
for _, v := range values {
v := v
go func() {
fmt.Println(v)
}()
}
<-time.Tick(time.Second)
}
无论是使用 for range 还是传统的 for i := 0; i < 3; i++ {},在 Go 1.22 及以后的版本中效果是相同的。
值得注意的是,由于 Go 的向下兼容性,如果你的 go.mod 文件中指定的 Go 版本是 1.22 之前,这个问题依然存在。你可以分别在 Go 1.22 和 1.22 之前的版本中运行上述代码,亲自验证这一点。
题外话:
在1.22版本中,for range 可以迭代int类型了!
下面这段代码遍历了从 0 到 9 的数字,等效于 for i := 0; i < 10; i++ {},只是语法更加简洁。这一语法糖让开发变得更加方便。
package main
import "fmt"
func main() {
for i := range 10 {
fmt.Println(10 - i)
}
fmt.Println("Go 1.22 has lift-off!")
}