WaitGroup 必须在启动 goroutine 前调用 Add(),否则可能 panic;需确保 Add() 与 Done() 配对,不可重复使用;动态任务需即时 Add();避免循环变量地址传递;用带缓冲 channel 控制并发数。
常见错误是把 wg.Add(1) 放在 goroutine 内部,导致 Wait() 等不到任何任务,或 Done() 调用次数超过 Add(),触发 panic:「sync: negative WaitGroup counter」。
正确做法是在启动 goroutine 之前调用 Add(),确保计数器已就绪:
var wg sync.WaitGroup
for _, task := range tasks {
wg.Add(1) // ✅ 必须在 go 前
go func(t string) {
defer wg.Done() // ✅ 必须配对
process(t)
}(task)
}
wg.Wait()wg.Add(1)
WaitGroup 不是锁,不能重复使用(即不能在 Wait() 后再次 Add()),除非重置为零值(不推荐)直接为每个任务启一个 goroutine 容易耗尽系统资源。用带缓冲的 channel 当“信号量”可限制最大并发数,比手动维护计数器更清晰可靠。
典型模式:一个 channel 作为令牌池,每启动一个任务先从 channel 取一个 token,完成后放回:
sem := make(chan struct{}, 5) // 最多 5 个并发
for _, task := range tasks {
sem <- struct{}{} // 阻塞直到有空位
go func(t string) {
defer func() { <-sem }() // ✅ 保证释放
process(t)
}(task)
}close(sem),它会导致后续 立即返回零值,破坏语义
WaitGroup 或额外 done channel单纯用 WaitGroup 无法安全收集返回值;只用 channel 可能因缓冲不足阻塞发送。两者结合最常用:W

关键点是结果 channel 应无缓冲或足够大,且所有 goroutine 统一发送后才关闭:
results := make(chan string, len(tasks))
var wg sync.WaitGroup
for _, task := range tasks {
wg.Add(1)
go func(t string) {
defer wg.Done()
res := process(t)
results <- res // ✅ 发送不阻塞(缓冲足够)
}(task)
}
go func() {
wg.Wait()
close(results) // ✅ 所有发送完成后关闭
}()
// 主协程接收
for res := range results {
fmt.Println(res)
}len(tasks),避免 sender 阻塞影响调度close(results),会 panic:「close of closed channel」Done() 不会被调用,Wait() 永不返回WaitGroup 和 channel 本身不感知超时或取消。实际项目中,必须用 context.Context 包裹任务,否则单个卡死任务会让整个流程 hang 住。
正确姿势是将 ctx 传入每个 goroutine,并在 I/O 或循环中定期检查:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
for _, task := range tasks {
wg.Add(1)
go func(t string) {
defer wg.Done()
select {
case <-ctx.Done():
log.Println("canceled:", t)
return
default:
processWithContext(ctx, t) // 在内部检查 ctx.Err()
}
}(task)
}
wg.Wait()cancel() —— 只有发起方才能决定取消时机ctx 参数,优先使用它们的上下文版本WaitGroup 和 channel 各有边界:前者管“是否做完”,后者管“如何通信与限流”。真正难的是在复杂流程中保持 context 传递、错误归并、以及 panic 恢复的统一处理——这些不会自动发生,得每一层都主动考虑。