死锁发生时Go运行时panic并打印fatal error,程序彻底卡死;通过panic日志中所有goroutine堆栈定位阻塞点,重点关注main goroutine停顿位置、channel操作及锁持有状态。

死锁发生时,Go 运行时会直接 panic 并打印 fatal error: all goroutines are asleep - deadlock! —— 这不是偶发卡顿,而是程序已彻底无法推进,必须立刻定位阻塞点。
看 panic 日志里的 goroutine 调用栈
Go 默认会在死锁时输出所有 goroutine 的当前堆栈,这是最直接的线索。重点盯三类信息:
-
maingoroutine 停在哪一行?比如卡在ch 或mu.Lock() - 其他 goroutine 是否全卡在同个 channel 操作(如都在
chan receive)或同一把锁上?说明资源被单点垄断 - 有没有 goroutine 卡在
select {}或空for循环里?那是典型的“忘了退出条件”
如果日志被截断,加环境变量运行:
GODEBUG=schedtrace=1000 go run main.go每秒输出调度快照,观察
g 数量是否长期为 0 —— 是,就确认全部阻塞。
查 channel 收发是否成对且有关闭方
无缓冲 channel 是死锁高发区:发送前必须有接收方就位,否则发送方永久阻塞;range 接收前必须有人 close,否则无限等待。
- 向未启动接收 goroutine 的 channel 发送 → 立即死锁
- 多个 goroutine 共用一个 channel,但由不同 goroutine 关闭 → 可能 panic 或漏数据
- 用
len(ch) == cap(ch)判断满、len(ch) == 0判断空,仅对有缓冲 channel 有效;无缓冲 channel 无法用长度判断可读/可写 - 必须确保只有一个 goroutine 负责
close(ch),且只 close 一次;接收方要配合value, ok := 判断是否已关闭
临时规避盲等:用 select { case ch 加 default 分支防阻塞。
审锁的获取顺序与生命周期
mutex 死锁不报错,但会让 goroutine 卡在 sync.(*Mutex).Lock,pprof 查到后得人工逆向分析谁拿了没放。
- 同一个 goroutine 连续调用两次
mu.Lock()(没Unlock)→ 直接卡死 -
defer mu.Unlock()写在 if 分支里,或提前return漏掉 → 锁永远不释放 - goroutine A 先锁
mu1再锁mu2,B 反过来先mu2后mu1→ 经典循环等待 - 持有锁期间调用可能阻塞的操作(如写 channel、HTTP 请求)→ 锁时间拉长,增加冲突概率
建议:给 mutex 字段加注释说明保护哪些变量;复杂逻辑优先用 channel 通信代替共享内存加锁。
用 pprof 和 -race 辅助验证
死锁不一定触发 runtime panic(比如部分 goroutine 阻塞但主 goroutine 还活着),这时需主动排查:
- 导入
_ "net/http/pprof",启动 HTTP 服务:go func() { http.ListenAndServe("localhost:6060", nil) }(),访问http://localhost:6060/debug/pprof/goroutine?debug=2查看实时堆栈 - 用
go run -race main.go检测数据竞争 —— 虽不直接报死锁,但竞态常是死锁前兆(比如两个 goroutine 都试图修改同一 map 而加锁顺序不一致) - 单元测试中模拟并发压测,CI 流水线里加
-race和超时限制,让死锁在集成阶段暴露
真正难缠的死锁往往只在高并发压测几小时后复现,靠日志和 pprof 快照很难抓到瞬间状态 —— 所以设计时就要避免“依赖对方先动”,比如用带缓冲 channel、设超时、加 context 控制生命周期,比事后调试更可靠。








