Go sync.WaitGroup在不同阶段安全复用 WaitGroup的约束
来源:17golang原创
时间:2026-09-20 02:20:23 272浏览 收藏
把一个 sync.WaitGroup 放进长期运行的 worker 或批处理器后,最容易出现的误区不是忘记调用 Done,而是把“下一阶段的 Add”插进“上一阶段的 Wait”里。安全复用的关键只有一句话:上一批任务全部结束、计数归零且 Wait 已返回之后,才能登记下一批任务。
官方文档:https://pkg.go.dev/sync#WaitGroup
- 同一个 WaitGroup 可以等待多个相互独立的任务批次,但批次之间必须有明确的 Wait 返回边界。
- 计数器为零时,正数 Add 必须发生在 Wait 之前;并发代码里最稳妥的做法是先 Add,再启动 goroutine。
- WaitGroup 不能复制;如果生产者和消费者的生命周期无法划分,应该重新设计批次或改用更合适的同步方式。
先划清一次使用的开始与结束
一次使用可以看成“登记任务、完成任务、等待归零”三个对象之间的契约。第一批调用 Add 后,goroutine 通过 Done 把计数减回零;此时阻塞中的 Wait 才能返回。复用不是把旧计数清空后随便继续,而是让上一批生命周期真正结束。

因此,下面这种外层循环是可以接受的:每轮先登记本轮全部任务,等待本轮归零,再进入下一轮。这里的“复用”指独立批次顺序复用,不代表多个调用者可以同时把任务塞进同一个计数器。
把 Add 放在启动 goroutine 之前
当计数器当前为零时,正数 Add 必须发生在 Wait 之前。工程上直接采用“先 Add、后 go”的固定顺序,能避免调度器让等待方抢先看到零计数:
package main
import "sync"
type Job struct {
Name string
}
func (j Job) Process() {
// 这里代表一项独立工作;真实项目中应返回或记录可处理的错误。
}
func runRound(jobs []Job) {
var wg sync.WaitGroup
wg.Add(len(jobs)) // 先登记本轮任务,避免 Wait 观察到过早的零计数
for _, job := range jobs {
job := job // 固定本轮迭代值,避免闭包共享循环变量
go func() {
defer wg.Done() // 无论任务如何结束,都对称回收一个计数
job.Process()
}()
}
wg.Wait() // 只有本轮所有 Done 到达后才返回
}

如果任务数量在启动后才知道,也不要让生产者在计数已经归零时偷偷追加。要么先收集任务再登记,要么让生产者本身也属于已登记的任务,并保证生产者退出前完成所有追加;这属于更复杂的生命周期设计,不能靠“多调用一次 Add”碰运气。
用 Done 对称回收每个任务
Add(n) 与 Done() 要能一一对应。把 Done 放在 goroutine 顶部的 defer 中,通常比在多个 return 前手写更可靠。任务失败也必须回收计数,否则主 goroutine 会一直卡在 Wait。
错误结果不要通过 WaitGroup 传递。可以使用带容量的 channel 收集错误,等待返回后再关闭它;若多个任务共享结果切片,还要额外使用互斥锁或让每个任务写入独立槽位。WaitGroup 只负责“是否全部结束”,不负责数据安全、错误聚合或取消传播。
把复用封装成批次函数
把复用边界藏在一个函数里,调用者就不会在 Wait 尚未返回时追加下一批。示例中每一轮都有自己的任务集合,上一轮返回后才登记下一轮:
func runRounds(rounds [][]Job) {
var wg sync.WaitGroup
for _, round := range rounds {
wg.Add(len(round)) // 本轮开始前完成全部计数登记
for _, job := range round {
job := job
go func() {
defer wg.Done() // 每个 goroutine 只负责减少自己的计数
job.Process()
}()
}
wg.Wait() // 返回后才允许下一轮再次 Add
}
}
这段写法表达的是串行批次:批次之间不重叠。如果业务实际需要批次重叠,就为每个批次创建自己的 WaitGroup,或者把并发模型改成明确的任务调度器,不要让一个复用中的计数器同时承担两种生命周期。
检查并发复用的边界
| 场景 | 判断 | 处理建议 |
|---|---|---|
| 上一轮 Wait 已返回,下一轮才 Add | 安全的独立批次复用 | 保留批次函数边界 |
| 计数器非零时动态 Add | 只有在生命周期约束明确时才可用 | 让追加方已被登记,写清退出条件 |
| 计数器为零时并发 Add 与 Wait | 存在 Wait 提前返回风险 | 改为先登记任务,再进入等待 |
| 复制已使用的 WaitGroup | 不允许 | 传指针或在新批次创建新的值 |
排查时先问三个问题:当前 Add 是否发生在对应的批次开始处;每个 goroutine 是否一定执行 Done;下一次 Add 是否等到上一次 Wait 返回。只要其中一个答案模糊,就不要把问题归结为“偶发调度”,而要重新画出任务生命周期。
相关问题
WaitGroup 可以在多轮任务中一直复用吗
可以,但前提是上一批所有 goroutine 已完成,并且所有等待者的 Wait 已经返回后,再开始下一批的 Add。
为什么先 go 再 Add 不稳妥
当计数器仍为零时,等待方可能先运行并直接返回;此时新 goroutine 才开始 Add,等待关系已经失效。
WaitGroup 能不能代替错误处理
不能。它只表示任务是否全部结束;错误、取消、共享数据保护需要 channel、context 或锁等独立机制。
-
472 收藏
-
163 收藏
-
324 收藏
-
171 收藏
-
496 收藏
-
188 收藏
-
123 收藏
-
479 收藏
-
375 收藏
-
337 收藏
-
279 收藏
-
372 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习