waitgroup.add()必须在goroutine启动前调用,add(n)需在循环外一次性完成;done()应使用defer确保执行;waitgroup必须传指针避免值拷贝导致的同步失效。

WaitGroup.Add() 必须在启动 goroutine 前调用
很多人把 Add() 放在 goroutine 里,结果 Wait() 立刻返回——因为主协程根本没等上,计数器还没来得及加。本质是竞态:Add 和 Done 的执行顺序不可控。
-
Add()必须在go func() {...}()之前调用,哪怕只是加 1 - 如果循环启多个 goroutine,
Add(n)要在循环外一次性加完,别在循环里边启边加(容易漏或重复) - 加负数会 panic:
panic: sync: negative WaitGroup counter,别传 -1 或变量未初始化就减
错误示例:
for i := range tasks {
go func() {
wg.Add(1) // ❌ 在 goroutine 里 Add,大概率失效
defer wg.Done()
process(i)
}()
}
wg.Wait() // 立刻返回
Done() 必须确保执行,且只能调用一次
Done() 是递减计数器,漏调、多调、或 panic 后没执行,都会让 Wait() 永远卡住,或触发 panic: sync: WaitGroup is reused before previous Wait has returned。
- 99% 的场景下,用
defer wg.Done()最安全,哪怕函数中途 return 或 panic - 别手动写
wg.Done()在多个 return 分支里——容易漏 - 绝不能对同一个
WaitGroup实例重复调用Wait(),除非中间调过Add()重置(但不推荐,易错)
正确姿势:
for _, task := range tasks {
wg.Add(1)
go func(t Task) {
defer wg.Done() // ✅ defer 保底
if err := t.Run(); err != nil {
log.Println(err)
return
}
}(task)
}
wg.Wait()
WaitGroup 不能复制,必须传指针
Go 中结构体赋值是值拷贝。WaitGroup 内部含 mutex 和 int64 计数器,复制后两个实例完全独立——你在副本上调 Add(),原实例的 Wait() 根本感知不到。
- 函数参数、字段、闭包捕获时,一律传
*sync.WaitGroup,不是sync.WaitGroup - 常见翻车点:闭包里直接引用外部 wg 变量,但该变量本身是局部值拷贝(比如从另一个函数返回了 wg 值)
- 编译器不会报错,但运行时行为诡异:有时等得到,有时死锁——取决于调度时机,极难复现
反例:
func startWorkers(wg sync.WaitGroup) { // ❌ 值传递
for i := 0; i < 3; i++ {
go func() {
wg.Add(1) // 修改的是副本
defer wg.Done()
}()
}
}
// 调用后 wg.Wait() 永远不返回
WaitGroup 不适合替代 channel 处理结果传递
WaitGroup 只解决“等结束”,不解决“拿结果”。强行用全局切片 + mutex 收集输出,代码臃肿且易出竞态;用 channel 才是 Go 的惯用法。
立即学习“go语言免费学习笔记(深入)”;
- 需要返回值、错误、或中间状态时,优先选
chan Result,而不是靠WaitGroup+ 全局变量 -
WaitGroup配合 channel 是常见组合:用 wg 控制 goroutine 生命周期,用 channel 流式收数据 - 别为了“统一用 wg”而放弃 channel —— 它们职责不同,混用反而增加心智负担
简洁做法:
results := make(chan Result, len(tasks))
for _, t := range tasks {
wg.Add(1)
go func(task Task) {
defer wg.Done()
results <- task.Run() // 直接发结果
}(t)
}
go func() { wg.Wait(); close(results) }() // 等完关 channel
for r := range results { // 安全接收
handle(r)
}
真正容易被忽略的是:WaitGroup 的零值可用,但一旦开始用,它的生命周期必须由使用者严格管理——Add/Done/Wait 的调用顺序、是否可重用、是否跨 goroutine 共享,每个环节都藏着静默故障点。写完记得问自己一句:如果某个 goroutine panic 了,wg.Done() 还会被调到吗?










