WaitGroup 计数器归零的唯一时机是 Wait() 返回之后;此时可安全重用,但需确保上一轮 Wait() 已完成,否则 Add() 与残留 Done() 交错会导致竞态或负计数 panic。

WaitGroup 的计数器什么时候归零?
计数器归零的唯一时机是 Wait() 返回之后——不是调用 Done() 之后,也不是所有 goroutine 结束之后,而是当 Wait() 内部检测到计数器变为 0 并唤醒等待者那一刻。
这意味着:即使你手动调用 wg.Add(-n) 把计数器减到 0,Wait() 也不会自动触发;只有在 Wait() 被调用且当时计数器为 0,它才立即返回。反过来,如果 Wait() 已返回,计数器就已归零,此时可安全重用。
-
Wait()返回后,sync.WaitGroup实例内部状态等价于刚声明的零值sync.WaitGroup{} - 若在
Wait()返回前再次调用Add(),可能和尚未执行的Done()竞态,导致负计数 panic - 不要依赖“goroutine 全部退出”来判断是否可重用——得看
Wait()是否已完成
为什么 wg.Add(1) 不能放在 goroutine 里?
因为 Add() 和 Done() 不是配对的原子操作,Go 运行时不校验调用顺序。一旦某个 Done() 在对应 Add(1) 之前执行(比如 goroutine 启动快、Add 慢),计数器就会变成 -1,立刻 panic:panic: sync: negative WaitGroup counter。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- 正确做法:循环中先
wg.Add(1),再go func() { ... wg.Done() }() - 错误写法示例:
go func() { wg.Add(1); defer wg.Done(); ... }()—— Add 在 goroutine 内,完全不可控 - 动态任务数(如从 channel 读取)时,需在外层预估数量或分批
Add(),不能靠“边收边加”
defer wg.Done() 在 panic 时失效怎么办?
defer 只在函数正常返回时执行。如果 goroutine 中发生未捕获 panic(比如 JSON 解析失败、空指针解引用),defer wg.Done() 就永远不会跑,计数器卡住,Wait() 永远阻塞。
立即学习“go语言免费学习笔记(深入)”;
- 显式调用更可靠:
go func() { defer func() { wg.Done() }(); ... }() - 或者加一层
recover:defer func() { if r := recover(); r != nil { /* log */ } wg.Done() }() - 避免在 goroutine 里直接 panic;HTTP handler 或数据库查询出错应返回 error,而非 panic
重用 WaitGroup 时最易忽略的竞态点
重用本身是安全的,但“上一轮还没结束,下一轮 Add() 就来了”才是真坑。典型场景是循环中反复使用同一个 wg 变量,但没等上一轮 Wait() 返回就开始下一轮 Add()。
- 错误模式:
for range items { wg.Add(1); go worker(); wg.Wait() }——Wait()在循环体内,但Add()在每次迭代开头,看似安全,实则可能因 goroutine 启动延迟导致下轮Add()和上轮残留Done()交错 - 安全模式:确保
Wait()完成后再进下一轮,或每次循环新建var wg sync.WaitGroup - 调试建议:加
fmt.Printf("add=%d, done=%d, wait=%v\n", ...)打印计数器变化(需用反射或私有字段,不推荐生产环境用);更实际的是启用-race检测

















