You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

向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, &currentJob)
}

内容的提问来源于stack exchange,提问作者Attilio

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 15:50:06