死锁发生时程序直接终止并输出 fatal error,无法用 recover 捕获;常见原因包括无缓冲 channel 同步收发、锁顺序不一致、rwmutex 误用;可用 go tool trace 定位阻塞点。

死锁发生时程序直接卡住,没有 panic 提示
Go 的 runtime 检测到所有 goroutine 都处于等待状态(比如都在等 channel 接收、都在等 mutex 解锁、或互相等待对方释放资源)时,会直接触发 fatal error:fatal error: all goroutines are asleep - deadlock!。它不会抛出可捕获的 panic,而是直接终止进程。这意味着你无法用 recover 拦截,必须靠逻辑排查或工具辅助定位。
常见诱因包括:
- 向无缓冲 channel 发送数据,但没有 goroutine 同时接收
- 从空的无缓冲 channel 读取,但没有 goroutine 同时发送
- 多个 goroutine 按不同顺序加锁两个
sync.Mutex,形成环路等待 - 在同一个 goroutine 中对已持有的
sync.RWMutex调用RUnlock多次,或误用Lock/RLock混搭
用 go tool trace 定位 goroutine 阻塞点
go tool trace 是 Go 自带的可视化分析工具,能精确看到每个 goroutine 的生命周期和阻塞原因。比单纯看日志或代码更可靠。
实操步骤:
立即学习“go语言免费学习笔记(深入)”;
- 在程序启动前加:
import _ "net/http/pprof",并在某处启动 pprof 服务(如http.ListenAndServe("localhost:6060", nil)) - 运行程序后,执行:
go tool trace -http=localhost:8080 ./your-binary - 打开浏览器访问
http://localhost:8080,点击 “View trace”,观察 Goroutines 视图中长时间处于 “Waiting” 或 “SyncBlock” 状态的 goroutine - 点击具体 goroutine,看其调用栈中卡在哪个函数——大概率是
chan send、chan recv、sync.(*Mutex).Lock等
注意:trace 文件默认只记录前 5 秒,若死锁发生较晚,需用 -duration=20s 手动延长采集时间。
channel 死锁的典型修复模式
无缓冲 channel 是死锁高发区。关键原则是:发送和接收必须由**不同 goroutine** 并发进行;若做不到,就别用无缓冲 channel。
错误写法(同步调用,必死锁):
c := make(chan int) c <- 1 // 卡在这里,永远等不到接收者 fmt.Println(<-c)
正确做法分场景:
- 确定有另一端接收 → 用 goroutine 包裹发送:
go func() { c - 不确定是否有接收者,或想避免阻塞 → 改用带缓冲 channel:
c := make(chan int, 1),此时c 不会阻塞 - 需要非阻塞尝试 → 用
select+default:select { case c - 单生产者单消费者且顺序固定 → 考虑用
sync.WaitGroup+ 共享变量替代 channel,减少调度开销
mutex 锁顺序不一致导致的死锁很难复现
当两个 goroutine 分别持有 A 锁再申请 B 锁、和持有 B 锁再申请 A 锁时,就构成经典“哲学家就餐”式死锁。这种问题在压力小的时候可能永远不暴露,一上生产环境并发量上来就偶发卡死。
预防手段比事后调试更重要:
- 始终按**全局约定顺序**获取多个锁,例如所有地方都先
muA.Lock()再muB.Lock() - 避免在持有锁期间调用外部函数(尤其是可能反过来调用本包其他加锁逻辑的回调)
- 使用
sync.Locker接口抽象锁行为时,确保底层实现不会隐式嵌套加锁 - 启用
go run -race可捕获部分锁竞争,但对纯死锁无效;可配合GODEBUG=mutexprofile=1运行后分析mutex prof输出
真正麻烦的是那些跨包、跨 goroutine、带 callback 的锁调用链——它们往往藏在第三方库或中间件里,需要结合 pprof mutex 和代码审计双管齐下。










