go运行时panic“all goroutines are asleep - deadlock!”仅在所有goroutine均阻塞且无runnable状态时触发,典型场景是无缓冲channel发送无人接收、单goroutine先发后收、sync.rwmutex读锁重入等,此时程序已彻底停摆,必须依据panic日志中的goroutine堆栈定位阻塞点。

Go 运行时本身就能报出最典型的死锁,但仅靠它远远不够——它只在所有 goroutine 都卡死时才 panic,而很多真实问题发生时程序还在“跑”,只是逻辑卡住、响应变慢、goroutine 泄漏。
运行时 panic “all goroutines are asleep” 能抓到什么
这个错误是 Go 最底层的保底机制,不是预测,而是确认程序已完全停摆。它只在满足两个条件时触发:所有 goroutine 都处于阻塞状态,且没有一个处于 runnable 状态。
常见触发点包括:
- 向无缓冲
chan int发送数据,但没有 goroutine 在收(如ch 后无接收) -
sync.WaitGroup.Wait()调用后,WaitGroup计数始终不为 0(漏调Done()或根本没Add()) - 在持有
sync.Mutex的 goroutine 中,再次调用Lock()(非重入锁,会永久阻塞,但 runtime 不认为这是“死锁”,因为该 goroutine 状态仍是 running)
注意:select {} 单独存在也会触发该 panic,因为它让 goroutine 永久阻塞且不可唤醒。
pprof /debug/pprof/block 是定位隐性阻塞的关键
比起等 panic,更实用的是主动看谁卡在哪。/debug/pprof/block 显示的是因同步原语(channel、mutex、cond、semaphore)而被阻塞的调用栈,单位是纳秒级阻塞时间总和。
实操建议:
- 启动服务前加
import _ "net/http/pprof",并在main()开头起一个 HTTP server:go http.ListenAndServe("localhost:6060", nil) - 复现问题后访问
http://localhost:6060/debug/pprof/block?debug=1,重点关注阻塞时间长、调用栈深的项 - 若看到大量阻塞在
chan send或chan receive,说明 channel 使用不匹配;若集中在sync.(*Mutex).Lock,要检查锁的持有链和释放时机
它不告诉你“这是死锁”,但能清晰指出:哪段代码、哪个 channel、哪个锁,正在拖慢整个系统。
go tool trace 查 channel 收发配对关系
当怀疑 channel 两端没对上(比如 sender 发了但 receiver 没启、或 receiver 提前退出),go tool trace 是唯一能看清“谁在发、谁在收、buffer 是否有剩”的工具。
关键操作步骤:
- 运行程序时加上 trace 支持:
go run -gcflags="all=-l" main.go(避免内联干扰追踪) - 程序中插入
trace.Start(os.Stderr)和defer trace.Stop(),或用 HTTP 方式导出:go tool trace http://localhost:6060/debug/trace?seconds=5 - 打开 Web UI 后,进
Goroutine标签页,筛选状态为chan send或chan receive的 goroutine,点击查看详情中的ch地址和调用位置
特别注意:chan send 阻塞在无缓冲 channel 上,但对应 chan receive goroutine 已 exit 或 panic —— 这类问题 runtime 不报 deadlock,但 trace 能一眼看出“发送端悬空”。
go vet 和 -race 对死锁帮助有限但不能跳过
go vet 只能识别极少数静态可判定的死锁模式,比如同一 goroutine 中连续写再读无缓冲 channel,或未初始化就用 channel。它无法发现跨 goroutine 的逻辑错配。
go run -race 不检测死锁,但它能暴露数据竞争——而数据竞争往往是死锁的前置症状(例如两个 goroutine 同时修改 channel 关闭状态,导致一方误判 channel 已关闭而提前退出)。所以:
- 开发阶段必跑
go vet ./...和go test -race - 但别指望它们告诉你“这里会死锁”,它们的作用是帮你剔除并发逻辑里更底层的不确定性
- 真正排查 channel 死锁,还是要回到
pprof/block和trace
复杂点在于:死锁往往不是单点错误,而是多个 goroutine 协作逻辑断裂的结果。你看到的阻塞 goroutine,常常只是“最后一根稻草”,真正的问题可能在上游某个 goroutine panic 后没通知下游、或 channel 被意外关闭却没人处理 closed 状态。查的时候得顺着阻塞点往回推调用链和生命周期,而不是只盯着 panic 那一行。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











