
本文详解 Go 中因 WaitGroup 值传递引发的典型死锁:fatal error: all goroutines are asleep - deadlock!,指出根本原因是 wg 被按值传入 goroutine,导致 Done() 作用于副本而非主 WaitGroup,Wait() 永远无法返回。
本文详解 go 中因 waitgroup 值传递引发的典型死锁:`fatal error: all goroutines are asleep - deadlock!`,指出根本原因是 `wg` 被按值传入 goroutine,导致 `done()` 作用于副本而非主 `waitgroup`,`wait()` 永远无法返回。
在 Go 并发编程中,sync.WaitGroup 是协调多个 goroutine 执行完成的核心工具。但其使用有严格约束:必须通过指针传递,否则将因值拷贝导致不可逆的同步失效。
你提供的代码中存在一个经典陷阱:
go MoveFile(src, dst, wg) // ❌ 错误:传入 wg 的值拷贝
MoveFile 函数签名 func MoveFile(src, dst string, wg sync.WaitGroup) 接收的是 sync.WaitGroup 的值类型参数。这意味着每次调用时,Go 都会复制整个 WaitGroup 结构体(包含内部计数器和互斥锁)。goroutine 内部执行的 defer wg.Done() 实际是在操作这个独立副本,而主线程中 wg.Wait() 等待的仍是原始 WaitGroup —— 其计数器从未被减少,最终所有 goroutine 结束后,Wait() 仍在无限等待,触发死锁。
✅ 正确做法是传递指针:
func MoveWorksheets(worksheetList []string) {
var wg sync.WaitGroup
for _, worksheet := range worksheetList {
wg.Add(1)
go MoveFile(src, dst, &wg) // ✅ 传入 *sync.WaitGroup
}
wg.Wait() // 阻塞直到所有 goroutine 调用 Done()
}
func MoveFile(src, dst string, wg *sync.WaitGroup) error { // ✅ 参数改为 *sync.WaitGroup
defer wg.Done() // ✅ 在原始 wg 上调用 Done()
// ... 文件移动逻辑(保持不变)
return MoveFileContents(src, dst)
}
⚠️ 其他关键注意事项:
- wg.Add() 必须在 go 语句前调用:确保计数器在 goroutine 启动前已更新,避免竞态(如 Add 和 Done 顺序错乱)。
- 不要在 goroutine 外部调用 wg.Done():Done() 应严格由对应 goroutine 自行调用(通常用 defer 保证)。
- WaitGroup 不可复制或重用:即使修复了指针问题,也不应在 Wait() 后再次 Add() 并复用同一实例(除非显式重置,但不推荐;建议作用域内新建)。
- 错误处理需谨慎:当前 MoveFileContents 中使用 log.Fatal() 会直接终止整个程序,破坏并发模型。应改为返回错误并由调用方统一处理:
func MoveFileContents(src, dst string) error {
if err := os.Rename(src, dst); err != nil {
return fmt.Errorf("failed to rename %s to %s: %w", src, dst, err)
}
return nil
}
总结:sync.WaitGroup 是引用语义的同步原语,其正确性完全依赖指针传递。牢记“*Always pass sync.WaitGroup, never sync.WaitGroup**”这一准则,可规避 90% 以上的 WaitGroup 相关死锁问题。











