标题
Go 死锁排查 Checklist:从报错到定位的实用手册
副标题 / 摘要
一页式清单,帮助你在看到 all goroutines are asleep - deadlock! 时,
快速定位是哪一类等待造成卡死。
目标读者
- 初学者:首次遇到 deadlock,不知道从哪下手。
- 中级开发者:需要可复用的排查流程,缩短定位时间。
- 团队负责人:希望沉淀成团队规范,避免重复踩坑。
背景 / 动机
死锁往往发生在高并发与多协作场景,复现难、定位慢。 有一份稳定的排查清单,可以把“凭直觉猜”变成“按步骤验证”。
核心概念
- deadlock 报错:所有 goroutine 都在等待,程序无法推进。
- 堆栈定位:栈上出现
<-ch/ch <-/mu.Lock()/wg.Wait()。 - 依赖闭环:等待关系形成环,导致无人能继续执行。
实践指南 / 步骤
1️⃣ 确认报错与堆栈是否完整
- 记录
fatal error: all goroutines are asleep - deadlock!后的完整堆栈。 - 优先关注 main goroutine 的等待点。
2️⃣ 分类定位阻塞类型
- channel:
<-ch/ch <- - WaitGroup:
wg.Wait() - Mutex:
mu.Lock()/RWMutex的读写锁等待
3️⃣ 检查等待关系是否闭环
- A 等 B,B 等 C,C 再等 A
- 多锁场景优先看锁顺序是否一致
4️⃣ 核对计数与配对关系
- WaitGroup:Add 与 Done 是否等量
- channel:发送者/接收者是否配对
5️⃣ 复现与最小化
- 抽取最小可复现场景
- 去掉无关逻辑,集中复现死锁点
可运行示例
下面示例演示如何主动打印 goroutine 栈(用于非 runtime deadlock 的卡死场景):
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
go func() {
for {
time.Sleep(2 * time.Second)
buf := make([]byte, 1<<16)
n := runtime.Stack(buf, true)
fmt.Println(string(buf[:n]))
}
}()
select {} // 模拟永久阻塞
}
解释与原理
- 堆栈是最重要的证据:deadlock 报错后,堆栈就是“案发现场”。
- 分类比盲查更快:先确定是 channel、WaitGroup 还是 mutex,再去找配对关系。
- 最小化复现:能把问题从复杂业务中剥离出来,减少误判。
常见问题与注意事项
- Q:没有 deadlock 报错,但程序卡住了?
A:可能是 goroutine 没全部阻塞,需用runtime.Stack或 pprof 排查。 - Q:加缓冲能解决吗?
A:缓冲只是延后阻塞,闭环仍在。 - Q:WaitGroup 为什么最常见?
A:Add 在主协程,Done 在子协程,最容易遗漏。
最佳实践与建议
- 先对齐收发,再考虑优化:无缓冲 channel 必须保证收发存在。
- 写清楚 Done 责任:谁 Add 谁确保 Done。
- 统一锁顺序:多锁场景的顺序必须固定。
- 为协程设计退出路径:防止永远等待。
小结 / 结论
死锁排查的关键是:确认等待点、分类阻塞类型、查找依赖闭环。 按清单执行,基本能在短时间内定位根因。
参考与延伸阅读
元信息
- 阅读时长:约 6 分钟
- 标签:Go、并发、死锁、排查、Checklist
- SEO 关键词:Go 死锁排查、deadlock、goroutine 堆栈、WaitGroup、channel
- 元描述:一页式 Go 死锁排查清单,覆盖堆栈分析、等待分类与闭环定位方法。
行动号召(CTA)
如果你有一段死锁堆栈或可复现代码,发出来,我可以帮你做具体定位。