目录

Go 踩过的坑之协程参数不能过大

一、问题

在日常开发过程中,我们通常会使用协程来并发处理数据,比如下面的例子采用 协程 + sync.WaitGroup 来并发处理数据:

从mysql请求的切片数据,遍历后开启协程根据指定字段统计数据,此处采用了协程组的方式提高效率

代码如下:

//结构体
type RespStruct struct{
    struct01 //内嵌另外的结构体
    struct02 //内嵌另外的结构体
}
func StatisticData(){
    var respData []RespStruct
    var wg sync.WaitGroup
    //1.从mysql查询数据,封装到respData ,代码省略
    //2.循环respData并且开启协程组,添加协程
    for i, d := range respData {
        wg.Add(1)
         go func(req ProduceOrderProcessQueryRespParam) {
            defer func() {
                wg.Done()
            }()
            //打印参数
             fmt.Println(req)
            return
        }(d)
    }
    wg.wait()
    fmt.Println("end.....")
}

输出报错:

fatal error: newproc: function arguments too large for new goroutine

二、原因

由于结构体嵌结构体,变成新的比较大的结构体d。那么在启动新协程的时候,又因为是值传递,新copy了一份d的副本,导致参数超过了新goroutine的可用堆栈空间。

goroutine默认分配2k的内存

先搞懂 Go 协程的参数传递规则——和普通函数一样,协程的参数传递也是“值拷贝”。也就是说,当你给协程传递一个参数时,Go 会创建这个参数的副本,然后把副本传递给协程,而不是直接传递原数据的引用。

除了切片,以下场景传递大参数也会触发同样的问题:

  • 超大结构体(比如包含大量字段的业务数据结构体,大小超过 1 MB)。

  • 长字符串(Go 字符串是不可变的,传递时也会拷贝整个字符串内容)。

  • 数组(注意是数组,不是切片,数组传递时会拷贝整个数组的所有元素)。

三、解决

方案一:传递指针(推荐)

既然值拷贝会产生大量副本,那直接传递参数的指针即可。

指针本身大小固定(64 位系统是 8 字节),无论原参数多大,拷贝指针的开销都可以忽略不计。

方案二:拆分参数

如果大参数是可拆分的(比如大切片、大文件),可以将其拆分为多个小参数,分批次传递给协程处理。或者减少参数的大小,只传入需要使用的参数

适合原数据不需要完整使用的场景,比如按行处理大文件。

四、代码修改

将协程 的匿名函数修改为函数调用方式;代码修改如下

//结构体
type RespStruct struct{
    struct01 //内嵌另外的结构体
    struct02 //内嵌另外的结构体
}
func StatisticData(){
    var respData []RespStruct
    var wg sync.WaitGroup
    //1.从mysql查询数据,封装到respData ,代码省略
    //2.循环respData并且开启协程组,添加协程
    for i, d := range respData {
        wg.Add(1)
         go func(req *ProduceOrderProcessQueryRespParam) {
            defer func() {
                wg.Done()
            }()
            //打印参数
             fmt.Println(req)
            return
        }(&d)
    }
    wg.wait()
    fmt.Println("end.....")
}

正常输出

常见问题

Q1:大参数的“标准”是什么?多大算“过大”需要优化?

没有绝对标准,核心看“拷贝后是否触发内存压力”。

推荐两个参考值:

① 单个参数超过10MB(如100万条int切片);

② 协程数量多(超过10个)且参数超过1MB。

我通常在“参数>1MB且协程数>5”时就会优化,避免后期协程数增加导致OOM。

Q2:指针传参时,多个协程同时读会有并发安全问题吗?

仅读不写时绝对安全,因为指针只是指向数据,多个协程读不会修改数据;如果有协程写数据,就会出现数据竞争,需要加互斥锁(sync.Mutex)。示例:

var mu sync.Mutex

func processData(data *[]int) {
    mu.Lock()         // 加锁
    (*data)[0] = 100  // 写操作
    mu.Unlock()       // 解锁

    // 读操作无需加锁
    fmt.Println(len(*data))
}

Q3:通道传递大参数和指针传递,哪个性能更好?怎么选?

能上指针略优(少了通道的调度开销),但差异极小(微秒级),

选型主要看场景:

① 简单读场景(无并发写):选指针,更简洁;

② 有并发写或需要同步的场景(如生产者-消费者):选通道,天然安全;

③ 超大规模数据:选分块+通道,兼顾效率和安全。

总结

Go 协程参数过大的坑,本质是对“值拷贝”规则的忽视。

很多开发者刚接触 Go 并发时,会下意识地给协程传大参数,结果踩了内存的坑。

核心避坑指南:

  1. 优先用指针:大参数传递首选指针,简单高效,注意生命周期和并发安全。

  2. 拆分优化*:超大规模数据拆分成小部分处理,避免单次传递过大参数。

  3. 工具排查:用 pprof 监控内存,快速定位参数拷贝导致的内存问题。

最后提醒:并发编程时,不要想当然地传递参数,先想清楚“传递的是什么”“会不会产生大量拷贝”。

多做内存监控,很多坑其实早就能发现。

大家在使用 go 语言开发过程中还遇到过哪些问题吗?欢迎大家在评论区分享交流~~~

版权声明

未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!

本文原文链接: https://fiveyoboy.com/articles/go-learned-goroutine-1/

备用原文链接: https://blog.fiveyoboy.com/articles/go-learned-goroutine-1/