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 导致进程挂死的代价,这点开销不值得考虑。

相关阅读


本文作者西米,37 岁程序员,一个人加五个 AI 员工在跑一家公司。踩过的坑和真实账本都写在公众号「西米分享」。

完整代码、踩坑清单与最新进展,都在公众号。

📱 关注公众号「西米分享」