go起gorutine常见错误-循环变量捕获语义机制

129 阅读2分钟

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循环的变量iv。当for循环执行时,会快速地创建多个goroutine

3.根因:

但这些goroutine并不会立即执行,而是被放入调度器的队列中等待执行。当goroutine最终开始执行时,for循环已经结束,此时iv的值已经是循环结束后的最终值(i为数组的长度减 1,v为数组的最后一个元素)。所以,所有的goroutine都会打印出相同的iv值。

4.解决:

go1.21及以前:需要将形参传递进去,本质需要闭包在使用的时候,把参数固定下来,因为闭包=函数+引用环境

go1.22及以后:引入循环变量捕获语义机制,本质还是编译器层面的优化。编译器会为 for 循环中创建的 goroutine 会为每次迭代捕获循环变量的副本。

4.1 内存管理和生命周期

编译器创建的这些变量副本的生命周期与相应的 goroutine 绑定。只要 goroutine 还在执行,其捕获的变量副本就会一直存在于内存中,以确保 goroutine 能够正确访问这些变量的值。 其实通过对go底层的逃逸分析机制,能想到这部分变量副本会被分配到操作系统的堆上

4.2 兼容性保证

另外补一句,为了保证老代码兼容性,Go 提供了 GOEXPERIMENT=loopvar 环境变量。如果设置了该变量,编译器会采用旧机制(但我想应该也没人会用吧)。在升级到 Go 1.22 后,如果代码依赖旧行为,可以用它过渡。