1.错误code
package main
import (
"fmt"
"time"
)
func main() {
var m = []int{1, 2, 3, 4, 5}
for i, v := range m {
go func() {
time.Sleep(time.Second * 3)
fmt.Println(i, v)
}()// 注意 这里如果不传递形参,会导致i,v 一直是4和5
}
time.Sleep(time.Second * 10)
}
2.现象:
匿名函数没有接收任何参数,而是直接引用了for循环的变量i和v。当for循环执行时,会快速地创建多个goroutine。
3.根因:
但这些goroutine并不会立即执行,而是被放入调度器的队列中等待执行。当goroutine最终开始执行时,for循环已经结束,此时i和v的值已经是循环结束后的最终值(i为数组的长度减 1,v为数组的最后一个元素)。所以,所有的goroutine都会打印出相同的i和v值。
4.解决:
go1.21及以前:需要将形参传递进去,本质需要闭包在使用的时候,把参数固定下来,因为闭包=函数+引用环境。
go1.22及以后:引入循环变量捕获语义机制,本质还是编译器层面的优化。编译器会为 for 循环中创建的 goroutine 会为每次迭代捕获循环变量的副本。
4.1 内存管理和生命周期
编译器创建的这些变量副本的生命周期与相应的 goroutine 绑定。只要 goroutine 还在执行,其捕获的变量副本就会一直存在于内存中,以确保 goroutine 能够正确访问这些变量的值。
其实通过对go底层的逃逸分析机制,能想到这部分变量副本会被分配到操作系统的堆上。
4.2 兼容性保证
另外补一句,为了保证老代码兼容性,Go 提供了 GOEXPERIMENT=loopvar 环境变量。如果设置了该变量,编译器会采用旧机制(但我想应该也没人会用吧)。在升级到 Go 1.22 后,如果代码依赖旧行为,可以用它过渡。