pprof 查不到真死锁,因死锁时 runtime 直接 panic 并输出“all goroutines are asleep”,不会进入 pprof;若无该提示,则存在运行中 goroutine,应结合 sigquit dump 和 /debug/pprof/goroutine 分析阻塞点。

pprof 查不到死锁?先确认是不是真死锁
Go 的 pprof 默认不捕获“纯死锁”——它只记录正在运行、阻塞或休眠的 goroutine,而真正的死锁(所有 goroutine 全部阻塞且无任何可运行项)会让 runtime 直接 panic 并打印堆栈,根本不会进 pprof。你看到 pprof 里 goroutine 数暴涨、大量 semacquire 或 chan receive 状态,那大概率是活锁/资源争用,不是死锁。
实操建议:
- 先跑一次
go run -gcflags="-l" main.go(关内联,方便看真实调用栈) - 如果程序卡住不动且无输出,立刻 Ctrl+\ 发送
SIGQUIT:Go 运行时会强制 dump 所有 goroutine 当前状态到终端 - 检查 panic 信息是否含
fatal error: all goroutines are asleep - deadlock!—— 只有这个才是真死锁 - 没这句?说明还有 goroutine 在跑,只是卡在 I/O、channel、mutex 或 network 等阻塞点,该上
pprof了
用 net/http/pprof 抓 goroutine 阻塞现场
必须让程序暴露 HTTP 接口才能用 net/http/pprof,别试图在命令行程序里直接 import 就完事——它依赖 HTTP server 启动。
常见错误现象:
立即学习“go语言免费学习笔记(深入)”;
- 访问
http://localhost:6060/debug/pprof/goroutine?debug=2返回空或 404:没注册 pprof 路由,或端口被占,或服务根本没起来 - 返回内容全是
runtime.gopark:说明大量 goroutine 卡在系统调用或 channel 操作,得结合debug=1看摘要统计
实操建议:
- 在 main 函数开头加:
go http.ListenAndServe("localhost:6060", nil),然后import _ "net/http/pprof" - 卡住后立即 curl:
curl 'http://localhost:6060/debug/pprof/goroutine?debug=2' > goroutines.log - 重点关注重复出现的调用链,比如连续几页都是
sync.(*Mutex).Lock→main.processOrder→chan send,这就是锁+channel嵌套的经典陷阱
goroutine dump 里怎么看谁在等谁
Go 的 goroutine stack dump 不像 Java jstack 那样带 explicit “waiting for lock #123”,它靠位置和上下文推断依赖关系。关键不是找“谁在等”,而是找“谁没放手”。
使用场景:
- 两个 goroutine 分别卡在
mu.Lock()和mu.Unlock()?不可能——Unlock几乎不阻塞,卡在Unlock通常意味着你 unlock 了一个未 lock 的 mutex,已触发 panic(但可能被 recover 吞了) - 常见模式是 A goroutine 持有
mu1并尝试获取mu2,B goroutine 持有mu2并尝试获取mu1:dump 里你会看到两段 stack 都停在各自的第二个Lock()调用上
参数差异与性能影响:
-
debug=1返回聚合统计(如 “245 goroutines total, 192 insemacquire”),适合快速判断阻塞类型 -
debug=2返回全量 stack,体积大但可 grep,比如grep -A 5 -B 2 "(*Mutex).Lock" goroutines.log - 频繁抓取
debug=2对高并发服务有轻微 GC 压力,别设成定时轮询
为什么 defer mu.Unlock() 在 select 里会失效
这是死锁高频坑:在 select 中对 channel 操作加锁,但 defer mu.Unlock() 放在函数开头,导致 channel 阻塞时锁一直不释放。
示例代码问题所在:
func handle(c chan int, mu *sync.Mutex) {
mu.Lock()
defer mu.Unlock() // ← 这里!select 卡住时 defer 根本不执行
select {
case v := <-c:
process(v)
case <-time.After(time.Second):
return
}
}正确做法是把锁粒度收紧到真正需要互斥的代码段,并确保每条路径都能释放:
- 去掉开头的
defer,改用mu.Lock(); defer mu.Unlock()包裹具体临界区 - 如果
select里要读写共享数据,把mu.Lock()移到每个case内部,或用default+ 循环重试避免长期持锁 - 更稳妥的是用
context.WithTimeout控制整个操作生命周期,而不是依赖time.After做超时
复杂点在于:goroutine 阻塞状态和锁持有状态不在同一层抽象里,dump 看到的只是快照,无法自动还原因果链。你得手动对齐时间点、匹配 mutex 地址、追踪 channel 缓冲状态——这些没法靠工具全自动。










