Go语言使用for循环结合WaitGroup时切片元素修改结果不符合预期
问题根因
你这段代码的问题和Go版本无关,哪怕用最新版本运行也会得到全0的结果,核心原因有两点:
- 最直接的错误:修改的是值拷贝而非原元素
range遍历值类型切片时,返回的第二个变量是原切片元素的完整拷贝,和原切片里的元素是完全独立的两个内存地址。你传递的&f是这个临时拷贝的地址,goroutine修改的是临时变量的属性,完全不会影响原切片foos的内容,所以打印原切片时所有Result字段还是初始的0。 - 隐藏的并发坑:Go 1.22及更早版本的循环变量复用
如果你使用的是Go 1.22之前的版本,for range的循环变量f是所有迭代共用同一个内存地址的。哪怕你只是想修改拷贝的内容,三次循环传入的&f都是同一个地址,三个goroutine会并发修改同一个临时变量,最终也得不到三个独立的结果。
修复方案
要让原切片的元素被正确修改,必须拿到原切片元素的真实内存地址,常见的正确写法有两种:
方案1:用索引遍历,直接取原元素地址
放弃用range出来的值拷贝,直接通过切片索引访问原元素,取地址后传入goroutine:
package main import ( "fmt" "sync" ) type Foo struct { Name string Result int } func main() { foos := make([]Foo, 0) foos = append(foos, Foo{"a", 0}) foos = append(foos, Foo{"bb", 0}) foos = append(foos, Foo{"ccc", 0}) var wg sync.WaitGroup for i := range foos { wg.Add(1) // 直接传原切片第i个元素的真实地址 go func(f *Foo, wg *sync.WaitGroup) { f.Result = len(f.Name) wg.Done() }(&foos[i], &wg) } wg.Wait() fmt.Println(foos) // 输出 [{a 1} {bb 2} {ccc 3}] }
这里把&foos[i]作为goroutine参数传入,Go的函数传参是值传递,会在go语句执行时就把当前i对应的元素地址求值后拷贝给goroutine的f参数,哪怕是Go 1.22之前的版本,也不会出现循环变量复用的问题。
方案2:改用指针类型切片
如果业务允许,可以把切片改成[]*Foo类型,此时range返回的就是原元素的指针,不需要额外取地址:
package main import ( "fmt" "sync" ) type Foo struct { Name string Result int } func main() { // 切片元素为指针类型 foos := make([]*Foo, 0) foos = append(foos, &Foo{"a", 0}) foos = append(foos, &Foo{"bb", 0}) foos = append(foos, &Foo{"ccc", 0}) var wg sync.WaitGroup for _, f := range foos { wg.Add(1) // f本身就是原元素的指针,直接传入即可 go func(f *Foo, wg *sync.WaitGroup) { f.Result = len(f.Name) wg.Done() }(f, &wg) } wg.Wait() // 打印时需要解引用指针 for _, f := range foos { fmt.Print(*f) } // 输出 {a 1} {bb 2} {ccc 3} }
注意:不要在闭包中直接捕获循环变量,否则Go 1.22之前会出现所有goroutine拿到循环结束后的变量值的问题。哪怕用索引遍历,下面这种写法也会出错:
// 错误示例:Go 1.22之前会输出三个相同的结果 for i := range foos { wg.Add(1) go func() { // 闭包捕获了循环变量i,所有goroutine拿到的都是循环结束后的i=2 foos[i].Result = len(foos[i].Name) wg.Done() }() }
内容的提问来源于stack exchange,提问作者赵学智
相关产品推荐
相关产品推荐

