WaitGroup 最容易写错的三种写法
WaitGroup 最容易写错的三种写法是哪三种,这份清单只干一件事:让你对照自己代码里的 WaitGroup,五分钟内找出来。它们是计数器加错位置、Add 在 goroutine 里面、以及忘了传指针。
一、WaitGroup 最容易写错的三种写法之一:Add 写在 goroutine 内部
这是最经典的一种。
写法是把启动 goroutine 和 Add(1) 都塞进循环,但 Add 写在 goroutine 的函数体里。看起来挺合理:我起了几个就加几个。
问题在于主 goroutine 可能已经走到 Wait() 了,而其他 goroutine 还没来得及执行到那句 Add。这时候计数器还是零,Wait 直接返回,程序带着一堆没干完的活往下走了。
结果就是结果不全、日志缺行,而且时好时坏。
具体动作:把 Add 挪到启动 goroutine 之前,跟 go 关键字紧挨着。规则是「先加数,再开人」。
二、计数器加多了或者漏加了
第二种是数量对不上。
常见形态是循环里条件分支加了一次 Add,但实际启动 goroutine 的位置在分支外面;或者反过来,启动了两个但只加了一次。
数量对不上会出现两种坏结果:加多了会一直卡在 Wait 上不返回,加少了会提前返回。
漏加那种尤其难查,因为程序不报错,只是结果少了一块。
具体动作:让 Add 和 go 在源码上相邻,中间不要夹别的逻辑。这样数量对不对,扫一眼就能看出来。如果循环里有条件判断,把判断放在 Add 和 go 之前。
三、值传递而不是指针传递
第三种是 WaitGroup 被复制了。
写法是把 WaitGroup 当成参数传给一个函数,参数类型写的是值类型而不是指针。传进去的是副本,函数里 Done() 减的是副本的计数器,外面的那个纹丝不动。
表现就是主 goroutine 永远等不到,程序挂在那里。
同样的问题也会出现在结构体里:WaitGroup 作为字段放进结构体后,如果这个结构体被按值传递,字段也跟着被复制。
具体动作:WaitGroup 一律用指针传,而且不要放进会被复制的结构体里。这类工具类的类型,官方文档里通常都明确写了不能复制。
四、Done 没被执行到
严格说这是第四种,但属于同一类问题,一起说。
Done() 如果写在 goroutine 正常结束的路径上,那中间任何一处提前返回或者 panic,这句就执行不到了。计数器永远减不下去,Wait 永远不返回。
具体动作:defer wg.Done() 写在 goroutine 函数体的第一行。用 defer 而不是直接调用,能覆盖所有返回路径。
这条几乎零成本,但收益很大。
五、Wait 和 Add 的顺序混了
还有一种是把 Wait() 写在了启动循环的前面,或者放进了循环里。
Wait() 放进循环里,会变成每加一个就等一个,等于退化成串行执行,并发一点没省下。
具体动作:Wait() 在整个启动循环之后,且只写一次。
六、先查哪个,以及一个更省事的替代
如果时间有限,按这个顺序查。
第一查 Done 有没有用 defer——它决定的是程序会不会挂死,是最严重的一类。
第二查 Add 的位置是否在 go 之前——这个决定结果对不对,而且改起来最快。
第三查有没有值传递——这个不常见,但一旦出现就必挂,值得花一分钟扫一遍函数签名和结构体字段。
前面三种查完,数量对不上和顺序问题基本也顺带看清楚了。
如果只是想让一批活干完再继续,而且需要拿到每个的结果,那其实不一定要用 WaitGroup。
用一个带缓冲的 channel 收结果,收够数量就往下走,代码更短,也不会出现计数器加错的问题。等结果顺便就等到了结束。
WaitGroup 更适合的场景是:只关心「都干完了」,不关心每个的具体返回值。这份清单到这里就核完了——WaitGroup 最容易写错的三种写法,只要有一种在你代码里出现过,就值得从头再查一遍。## 常见问题
Q:WaitGroup 最容易写错的三种写法是什么?
A:Add 写在 goroutine 内部、计数器数量对不上、以及按值传递导致副本被减。前两种最常见,第三种一旦出现必然挂死。
Q:Add 为什么不能写在 goroutine 里?
A:因为主 goroutine 可能在它执行之前就已经走到 Wait 了。计数器还是零,Wait 立即返回,程序带着没干完的活继续跑。
Q:Done 用 defer 会有性能问题吗?
A:在 goroutine 这个粒度上可以忽略。相比漏掉一次 Done 导致进程挂死的代价,这点开销不值得考虑。
相关阅读
- Go 里 channel 和 mutex 该用哪个
- Go context 超时取消怎么一层层传下去
- Go 并发读写 map 报错,怎么改才安全
- Go 并发测试老是挂,我最后这么改稳的
- Go 并发编程的 5 个常见坑,新手先看
本文作者西米,37 岁程序员,一个人加五个 AI 员工在跑一家公司。踩过的坑和真实账本都写在公众号「西米分享」。
完整代码、踩坑清单与最新进展,都在公众号。
📱 关注公众号「西米分享」