sync.WaitGroup 值传递导致计数器失效,因每个goroutine操作独立副本,wg.Done()无效,引发死锁;应改用指针传递。Wait()不可置于select循环中,否则双向阻塞;通道关闭须由主goroutine在wg.Wait()后执行,避免panic或饥饿。

sync.WaitGroup 值传递导致计数器失效
这是最常见、也最容易被 go vet 检测出的死锁根源:把 sync.WaitGroup 当作值参数传给 goroutine 函数,结果每个 goroutine 操作的都是独立副本,wg.Done() 对主 goroutine 的计数器毫无影响。
典型错误写法:
func worker(wg sync.WaitGroup) { // ❌ 值传递
defer wg.Done()
// ... 工作逻辑
}
// 调用时:
wg.Add(1)
go worker(wg) // 传入的是 wg 的副本
后果是:wg.Wait() 永远卡住,报 fatal error: all goroutines are asleep - deadlock。
- Go 运行时禁止拷贝含
sync.Mutex的结构体,sync.WaitGroup内部就嵌了它 -
go vet会直接提示:passes sync.WaitGroup by value - 修复只需改成指针传递:
func worker(wg *sync.WaitGroup),调用时传&wg
WaitGroup.Wait() 放在通道 select 循环里
把 wg.Wait() 塞进 for { select { ... } } 循环中,会导致主 goroutine 在子 goroutine 还没发完数据时就反复阻塞等待,而子 goroutine 又因通道未被消费而卡在发送上 —— 典型的双向阻塞。
立即学习“go语言免费学习笔记(深入)”;
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
错误模式示例:
for {
select {
case <-AddChan:
// 处理
default:
wg.Wait() // ❌ 错误:每次循环都等,但此时 AddChan 可能还没被读
}
}
- 根本问题是混淆了“等待 goroutine 结束”和“等待通道有数据”两种语义
-
wg.Wait()应该只调用一次,且放在通道消费逻辑之后(或配合close()+range) - 无缓冲通道尤其危险:发送必须立刻被接收,否则 sender 永久阻塞
Add 和 Done 数量不匹配或顺序错乱
wg.Add() 必须在 go 启动前调用,且 Add(n) 与后续 n 次 Done() 必须严格对应。漏调、多调、提前调都会让计数器无法归零。
-
wg.Add(1)后没启动 goroutine →wg.Wait()立即死锁 - goroutine panic 未执行
defer wg.Done()→ 计数器卡住 - 在 goroutine 内部多次调用
wg.Add(1)却只调一次Done()→ 计数器永远 > 0 - 修复建议:始终用
defer wg.Done(),并在go语句前紧邻位置调用wg.Add(1)
并发关闭通道与 WaitGroup 协同不当
多个生产者 goroutine 共享一个通道时,谁负责 close()?如果由某个生产者在函数末尾 defer close(ch),很可能在其他生产者还没发完时就关了通道,导致后续发送 panic;如果没人关,消费者永远等不到 range 结束。
- 正确做法是:所有生产者完成后再由主 goroutine 关闭通道(需确保所有
Done()已触发) - 更安全的模式是:用
wg.Wait()等待所有生产者退出,再close(ch),然后消费者用for range ch - 避免在生产者内部关通道,除非你明确控制了全部生产者的生命周期(例如用带缓冲通道 + 单一 producer)
真正难处理的不是语法,而是计数器状态与 goroutine 生命周期之间的隐式耦合——一旦 Add/Done/wg.Wait 的时机或对象错位,程序不会报错,只会静默卡住。

















