WaitGroup 必须在启动 goroutine 前调用 Add(),否则可能 panic;需确保 Add() 与 Done() 配对,不可重复使用;动态任务需即时 Add();避免循环变量地址传递;用带缓冲 channel 控制并发数。

WaitGroup 必须在启动 goroutine 前 Add,否则可能 panic
常见错误是把 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()- 如果任务数动态变化(如从 channel 拉取),需在每次拉取后立即
wg.Add(1) -
WaitGroup不是锁,不能重复使用(即不能在Wait()后再次Add()),除非重置为零值(不推荐) - 避免在循环中传入循环变量地址,要用闭包参数传值,否则所有 goroutine 共享同一个变量
用 channel 控制并发数量,避免 Goroutine 泛滥
直接为每个任务启一个 goroutine 容易耗尽系统资源。用带缓冲的 channel 当“信号量”可限制最大并发数,比手动维护计数器更清晰可靠。
典型模式:一个 channel 作为令牌池,每启动一个任务先从 channel 取一个 token,完成后放回:
立即学习“go语言免费学习笔记(深入)”;
sem := make(chan struct{}, 5) // 最多 5 个并发
for _, task := range tasks {
sem <- struct{}{} // 阻塞直到有空位
go func(t string) {
defer func() { <-sem }() // ✅ 保证释放
process(t)
}(task)
}- 缓冲大小即最大并发数,适合 I/O 密集型任务(如 HTTP 请求、文件读写)
- 不要用
close(sem),它会导致后续立即返回零值,破坏语义 - 若需等待全部完成,仍需配合
WaitGroup或额外 done channel
WaitGroup + channel 混合使用:收集结果并控制完成时机
单纯用 WaitGroup 无法安全收集返回值;只用 channel 可能因缓冲不足阻塞发送。两者结合最常用:WaitGroup 管生命周期,channel 收结果。
关键点是结果 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)
}- 结果 channel 缓冲大小建议设为
len(tasks),避免 sender 阻塞影响调度 - 绝不能在 goroutine 内直接
close(results),会 panic:「close of closed channel」 - 若任务可能 panic,需在 goroutine 内 recover,否则
Done()不会被调用,Wait()永不返回
别忽略 context.Context:超时与取消必须由上层驱动
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()- 不要在 goroutine 中调用
cancel()—— 只有发起方才能决定取消时机 - HTTP client、database query、time.Sleep 等都支持
ctx参数,优先使用它们的上下文版本 - WaitGroup 不提供 cancel 能力,这是 context 的职责,二者分工明确
WaitGroup 和 channel 各有边界:前者管“是否做完”,后者管“如何通信与限流”。真正难的是在复杂流程中保持 context 传递、错误归并、以及 panic 恢复的统一处理——这些不会自动发生,得每一层都主动考虑。










