Go潜在陷阱:循环变量作用域的坑被填平了

498 阅读2分钟

我们知道,在 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!")
}