go 运行时检测到所有 goroutine 休眠且无法唤醒时 panic 并终止程序;预防需从 channel 生命周期、goroutine 协作逻辑和同步原语使用三方面入手,无缓冲 channel 发送前必须有并发接收者。
go 运行时检测到所有 goroutine 都在等待且无法唤醒时,会直接 panic 报 fatal error: all goroutines are asleep - deadlock!,这不是可 recover 的错误,程序立即终止。预防关键不是“加锁更小心”,而是从 channel 生命周期、goroutine 协作逻辑和同步原语使用习惯三处堵死源头。
无缓冲 channel 发送前必须有并发接收者
无缓冲 channel 的 send 操作(c )会永久阻塞,直到另一端有 goroutine 执行 <code>。main 中顺序写 <code>c 然后 <code>fmt.Println( 必然死锁——因为没有并发 goroutine 参与接收。
- 永远不要在单个 goroutine 内对无缓冲 channel 做发送 + 接收的顺序操作
- 启动接收 goroutine 必须早于发送:先
go func() { ,再 <code>c - 用带缓冲 channel 替代无缓冲 channel 仅能缓解,不能根治:若缓冲区满后仍无人消费,后续 send 一样阻塞
range 遍历 channel 前确保它会被关闭
for v := range ch 会一直等待 channel 关闭才退出。如果 sender 忘记 close(ch),或关闭过早(还有 goroutine 尝试发送),就会卡住或 panic。
- 只有 sender 有权 close channel;receiver 调用
close(ch)是错误用法 - 多个 sender 时,用
sync.WaitGroup协调:所有 sender 完成后,由 wg.Done() 触发的最后一个 goroutine 调用close(ch) - 更安全的替代写法是
for { v, ok := ,避免依赖 close 时机
sync.Mutex 重复 Lock 或交叉加锁极易触发死锁
sync.Mutex 不支持重入。同一个 goroutine 连续两次 mu.Lock()(中间没 Unlock()),或 goroutine A 锁 m1 后等 m2、goroutine B 锁 m2 后等 m1,都会导致运行时判定为死锁并 panic。
- 统一用
defer mu.Unlock(),且只在Lock()后立刻写,不要跨分支手动解锁 - 多锁场景下,按固定顺序加锁:例如始终先
m1.Lock()再m2.Lock(),哪怕某次只用到其中一个 - 避免在持有锁期间调用可能间接获取同一把锁的函数(包括方法调用),否则容易形成隐式嵌套
WaitGroup 计数不匹配会让 main 卡在 wg.Wait()
wg.Wait() 会阻塞直到内部计数归零。如果 wg.Add(n) 漏调、wg.Done() 被 panic 跳过、或 Done() 放在 goroutine 逻辑末尾而非入口 defer,就永远等不到结束。
-
wg.Add(n)必须在go启动前完成,不能在 goroutine 内动态 Add - 每个 goroutine 入口第一行写
defer wg.Done(),确保无论是否 panic 都会减计数 - 别把
sync.WaitGroup当全局变量传;若需跨包协作,封装进结构体字段并提供 Start/Stop 方法控制生命周期
真正难调试的不是报 panic 的死锁,而是那些 goroutine 泄漏后长期阻塞在 channel 上、CPU 归零但内存缓慢上涨的情况——它不触发 runtime 死锁检测,却让服务逐渐失联。上线前务必用 runtime.NumGoroutine() 监控数量趋势,配合 pprof/goroutine?debug=2 抓取阻塞栈,才能暴露这类“静默死锁”。











