waitgroup 是计数器+信号量组合,需手动 add/ done/ wait;常见误用包括 add 位置错误、未 add 就 done 导致 panic;不可复用除非严格串行完成;不支持超时,需配合 context 或 select 实现。

WaitGroup 的基本用法和常见误用
WaitGroup 不是“等待所有 goroutine 结束”的自动魔法,它只是计数器 + 信号量的组合。你必须手动调用 Add 增加计数、Done 减少计数,最后用 Wait 阻塞直到归零。
最常踩的坑是:在 goroutine 外部调用 Done,或者在未调用 Add 的情况下调用 Done —— 这会直接 panic:panic: sync: negative WaitGroup counter。
-
Add必须在启动 goroutine 之前(或至少在Done之前)调用,且参数不能为负 -
Done等价于Add(-1),只能在 goroutine 内部安全调用 -
Wait可被多次调用,但一旦计数归零,后续Wait立即返回,不会重置计数 - 不要把
WaitGroup当作锁用;它不保证 goroutine 执行顺序,也不保护共享数据
为什么不能在 goroutine 启动后才 Add?
因为 go func() { ... }() 是异步的,调度不可控。如果在 go 语句之后才调用 Add(1),主线程可能已执行到 Wait 并提前返回,而 goroutine 还没来得及运行 Done —— 导致程序提前退出,goroutine 被强制终止。
正确做法是:先 wg.Add(1),再 go func() { defer wg.Done(); ... }()。
// ✅ 正确
wg.Add(1)
go func() {
defer wg.Done()
// do work
}()
// ❌ 危险:Add 在 go 后,竞态风险高
go func() {
// do work
wg.Done()
}()
wg.Add(1) // 这里 Add 已晚,Wait 可能早已返回
WaitGroup 和 context.WithTimeout 组合使用时要注意什么?
WaitGroup 本身不支持超时。如果想实现“等最多 5 秒,超时就放弃”,不能只靠 Wait,必须配合 context 或 channel select。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
典型错误是:在 Wait 上阻塞,同时另起 goroutine 发送 cancel —— 这无法中断 Wait,因为 Wait 没有上下文感知能力。
- 用
select+donechannel 实现带超时的等待(Wait自身不返回 channel) - 可封装一个辅助函数:
WaitWithTimeout(wg *sync.WaitGroup, timeout time.Duration) bool,内部用sync.Once避免重复 close - 注意:超时后
WaitGroup计数仍为 0(如果所有 goroutine 已完成),或仍大于 0(部分未完成)—— 你无法“取消”正在运行的 goroutine,只能选择忽略它们
WaitGroup 能否复用?要不要重置?
官方文档明确说:WaitGroup 可以复用,但必须确保在所有 goroutine 都结束、且 Wait 返回后,才能再次调用 Add。没有 Reset 方法,也不能靠赋值 sync.WaitGroup{} 来“清空”——这会破坏内部状态,引发 panic。
复用场景常见于循环任务(如 worker pool 中每轮处理一批 job),但必须满足:上一轮 Wait 已返回,且所有 Done 已执行完毕。
- 不要在
Wait还没返回时,对同一个WaitGroup调用第二次Add - 不要用
wg = sync.WaitGroup{}试图重置;这是无效的,且可能导致 data race - 若逻辑复杂难保证复用安全,不如每次新建一个
sync.WaitGroup,开销极小
真正容易被忽略的是:WaitGroup 内部使用了 unsafe.Pointer 和原子操作,它的零值是有效的,但“复用”不是指反复 new,而是指在严格串行完成周期后继续用同一个实例 —— 这个边界稍有不慎就会出问题。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










