最常见的死锁情况是:一个或多个goroutine在select或for range中等待channel关闭,而发送方未显式close(channel),导致接收方永久阻塞;无缓冲channel收发不同步、循环等待、错误关channel或锁顺序不一致也是主因。

goroutine 等待 channel 关闭却没人关它
死锁最常见的情况是:一个或多个 goroutine 在 select 或 上永久阻塞,而写入方已退出、忘记关闭、或根本没启动。Go 运行时检测到所有 goroutine 都在等待(没有可运行的),就 panic 报 <code>fatal error: all goroutines are asleep - deadlock!。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 确认每个被读取的
chan int(尤其是无缓冲 channel)都有且仅有一个明确的写入方,并保证它终会退出或调用close(ch) - 读端别盲目用
for range ch—— 如果 channel 永远不关,循环永不结束;改用for v, ok := 更可控 - 测试时加超时:用
select { case v := ,避免测试卡死
sync.Mutex 重复 Lock 或忘 Unlock
不是 channel 专属问题,但并发安全排查里高频踩坑:同一个 sync.Mutex 在同一线程多次 Lock()(没配对 Unlock()),会导致后续 goroutine 永远等不到锁,最终全卡住。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 绝不在 defer 前手动
mu.Unlock(),也别在多个分支里各自写Unlock()—— 统一用defer mu.Unlock(),哪怕中间有 return - 检查嵌套调用:函数 A 调 B,A 和 B 都试图 Lock 同一把锁?这是典型重入死锁(Go 的
sync.Mutex不支持重入) - 用
-race编译运行:它能报出 “Previous write at … by goroutine N” 和 “Current read at … by goroutine M”,比死锁更早暴露竞争
WaitGroup 计数没归零就 Wait
sync.WaitGroup 的 Wait() 会阻塞直到内部计数为 0。如果 Add() 多了、Done() 少了,或者 Done() 被异常路径跳过(比如 panic 后 defer 没触发),就会永远卡在 wg.Wait()。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 初始化后立刻
wg.Add(n),而不是在 goroutine 里动态 Add —— 避免漏加或重复加 - 每个 goroutine 入口第一行写
defer wg.Done(),不要放在逻辑末尾 - 别把
wg当全局变量传;如果必须跨包,考虑封装成结构体字段 + 方法,确保生命周期清晰
context.WithCancel 被提前 Cancel 导致协程静默退出
这不是传统意义的死锁,但常被误判为“程序卡住”:主 goroutine 调了 cancel(),子 goroutine 因 select { case 立即退出,但主 goroutine 又在等它们通过 <code>wg.Wait() —— 此时若子 goroutine 已退、wg 计数未减完,就真死锁了。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- cancel 函数只由单一责任方调用(通常是发起者),别在多个 goroutine 里乱传乱调
- 用
ctx.Err()判断退出原因,区分context.Canceled和context.DeadlineExceeded,避免错误归因 - 组合使用:带 cancel 的 ctx + wg 是常见模式,但务必保证
wg.Done()在 defer 中,且位于ctx.Done()select 分支之后(即先清理再通知完成)
真正难排查的死锁,往往藏在跨 goroutine 的状态耦合里:A 等 B 关 channel,B 等 A 调 Done,A 又在等 B 的 mutex…… 这时候单看某一行代码都对,合起来就锁死。多打日志、少信直觉、用 -race 和 pprof trace 辅助定位。











