
本文详解go中因waitgroup值传递导致的死锁问题,指出goroutine间同步失败的根本原因,并提供两种正确解决方案:指针传递waitgroup或直接使用全局变量。
本文详解go中因waitgroup值传递导致的死锁问题,指出goroutine间同步失败的根本原因,并提供两种正确解决方案:指针传递waitgroup或直接使用全局变量。
在Go并发编程中,sync.WaitGroup 是协调多个goroutine执行完成的核心工具。但若使用不当——尤其是错误地以值方式传递 WaitGroup 实例——将导致 wg.Done() 在副本上调用,而主线程等待的原始 WaitGroup 计数器永不归零,最终触发死锁。
上述代码的死锁根源正在于此:函数 sqr(ch, val, wg sync.WaitGroup) 的第三个参数声明为值类型,每次调用都会复制一份 wg。goroutine 内执行 wg.Done() 仅减少该副本的计数器,对 main 中的原始 wg 完全无影响。因此 wg.Wait() 永远阻塞,所有goroutine执行完毕后主线程无法继续,Go运行时判定“所有goroutine休眠”,抛出 fatal error: all goroutines are asleep - deadlock!。
✅ 正确做法一:传递 WaitGroup 指针
func main() {
numOfGoRoutines := 10
wg.Add(numOfGoRoutines)
ch := make(chan int, numOfGoRoutines)
for i := 0; i < numOfGoRoutines; i++ {
a := i
go sqr(ch, a, &wg) // 传指针:&wg
}
wg.Wait()
fmt.Println("After WAIT")
close(ch)
res := 0
for v := range ch {
res += v
}
fmt.Println("result =", res)
}
func sqr(ch chan int, val int, wg *sync.WaitGroup) { // 接收 *sync.WaitGroup
fmt.Println("go -", val)
ch <- val * val
wg.Done() // 修改原始 wg 实例
}⚠️ 注意:sync.WaitGroup 不是线程安全的指针类型,但其方法(Add, Done, Wait)本身已内置同步机制,因此安全地传递指针是标准且推荐的做法。
✅ 正确做法二:利用全局变量(简洁但需谨慎)
既然 wg 已声明为包级全局变量,可直接在 sqr 中引用,无需参数传递:
func sqr(ch chan int, val int) {
fmt.Println("go -", val)
ch <- val * val
wg.Done() // 直接操作全局 wg
}此时 main 中调用变为 go sqr(ch, a),彻底规避参数传递问题。
? 补充关键细节
- Channel 缓冲区大小匹配:make(chan int, numOfGoRoutines) 设置了足够缓冲,确保10个 ch <- s 不会阻塞,这是本例能顺利发送结果的前提。
- 关闭 channel 的时机:必须在所有发送完成后(即 wg.Wait() 返回后)调用 close(ch),否则 for range ch 会永久等待。
- 避免 nil channel 赋值:ch = nil 并非必需,且对已关闭的 channel 无实际意义,建议移除以提升代码清晰度。
✅ 最终验证效果
修复后程序输出:
go - 0 go - 1 go - 2 ... go - 9 After WAIT result = 285
(0²+1²+...+9² = 285),逻辑与并发行为完全符合预期。
总结:Go中 sync.WaitGroup 必须通过指针共享状态;值传递会导致同步失效和死锁。牢记 “传递指针,而非值” 这一原则,是编写健壮并发程序的基础准则。

















