向Goroutine传递结构体指针时出现异常行为的原因是什么?
Go中Goroutine传递结构体引用时的变量错误问题解析
TL;DR
向Goroutine传递结构体变量的引用时,出现引用错误变量的问题,核心原因是range循环的迭代变量复用机制,结合Goroutine的异步执行特性导致的。
问题复现
以下代码会出现不符合预期的输出:
package main import ( "fmt" "time" ) type Job struct { foo int } func main() { var jobs []Job jobs = []Job{} var jobsCount = 10 for i := 0; i < jobsCount; i++ { jobs = append(jobs, Job{foo: i}) } fmt.Println(jobs) for i, job := range jobs { fmt.Printf("Starting '%d' '%+v'\n", i, job) go func(i int, job *Job) { fmt.Printf("Processing '%d' job '%+v'\n", i, job) }(i, &job) } time.Sleep(10 * time.Second) }
输出示例:
[{0} {1} {2} {3} {4} {5} {6} {7} {8} {9}] Starting '0' '{foo:0}' Starting '1' '{foo:1}' Starting '2' '{foo:2}' Starting '3' '{foo:3}' Starting '4' '{foo:4}' Starting '5' '{foo:5}' Starting '6' '{foo:6}' Starting '7' '{foo:7}' Starting '8' '{foo:8}' Starting '9' '{foo:9}' Processing '7' job '&{foo:9}' Processing '3' job '&{foo:7}' Processing '2' job '&{foo:4}' Processing '6' job '&{foo:8}' Processing '0' job '&{foo:4}' Processing '9' job '&{foo:9}' Processing '4' job '&{foo:6}' Processing '8' job '&{foo:9}' Processing '5' job '&{foo:6}' Processing '1' job '&{foo:4}'
可以看到:
- 多个Goroutine处理的是同一个
{foo:9}结构体,而部分结构体(比如{foo:1})没有被正确处理 - 循环本身正常遍历了所有元素,计数器
i也被正确传递给了Goroutine
问题根源
Go的for range循环有一个关键特性:迭代变量(这里的job)是在循环体外声明的,整个循环过程中复用同一个内存地址。也就是说,每次循环只是把当前切片元素的值复制到job变量中,而不是创建新的变量。
当你启动Goroutine并传递&job时,传递的是这个复用变量的地址。但Goroutine是异步执行的,它可能在循环已经执行了多次迭代后才开始运行——此时job变量已经被后续的循环迭代覆盖成了新的值,所以多个Goroutine会读到同一个(往往是最后几次迭代的)变量值。
而计数器i能正确传递,是因为在启动Goroutine时,i是按值传递给匿名函数的,每个Goroutine拿到的是当前迭代i的副本,不受后续循环的影响。
验证与解决
1. 按值传递结构体(可行)
如果把结构体按值传递给Goroutine,每个Goroutine拿到的是当前迭代job的副本,不受后续循环覆盖影响,输出就符合预期:
for i, job := range jobs { fmt.Printf("Starting '%d' '%+v'\n", i, job) go func(i int, job Job) { fmt.Printf("Processing '%d' job '%+v'\n", i, job) }(i, job) }
2. 同步调用函数(可行)
如果在同一个Goroutine中同步执行函数,函数会在job被下一次迭代覆盖前就读取它的值,所以能拿到正确的引用:
for i, job := range jobs { fmt.Printf("Starting '%d' '%+v'\n", i, job) func(i int, job *Job) { fmt.Printf("Processing '%d' job '%+v'\n", i, job) }(i, &job) }
3. 传递切片元素的真实地址(推荐)
直接传递切片中对应元素的地址,而不是迭代变量的地址,这样每个Goroutine拿到的是切片里元素的真实内存地址,不会被循环覆盖:
for i := range jobs { fmt.Printf("Starting '%d' '%+v'\n", i, jobs[i]) go func(i int, job *Job) { fmt.Printf("Processing '%d' job '%+v'\n", i, job) }(i, &jobs[i]) }
4. 循环内创建局部变量保存迭代值
在循环内部创建一个局部变量,保存当前迭代的job值,再传递这个局部变量的地址,避免复用问题:
for i, job := range jobs { currentJob := job fmt.Printf("Starting '%d' '%+v'\n", i, currentJob) go func(i int, job *Job) { fmt.Printf("Processing '%d' job '%+v'\n", i, job) }(i, ¤tJob) }
内容的提问来源于stack exchange,提问作者Attilio
相关产品推荐
相关产品推荐

