死锁发生时程序直接 panic,go 运行时无死锁检测 api,fatal error 即终局判决;常见误操作包括 time.sleep 掩盖问题、numgoroutine 误判状态、waitgroup 值传递;竞态必须用 go run -race 插桩捕获;/debug/pprof/block 是死锁前最准的阻塞快照;嵌套锁需固定获取顺序(如按地址排序)并避免锁内再加锁。

死锁发生时程序直接 panic,别试图提前检测
Go 运行时没有 runtime.IsDeadlocked() 这类 API,所谓“检测死锁”本质就是接受 fatal error: all goroutines are asleep - deadlock! 这个 panic 并分析堆栈。它不是预警信号,而是终局判决——程序已不可恢复。
常见误操作包括:
- 在
main末尾加time.Sleep()等 goroutine 完成:这会让主 goroutine 主动卡住,但只要还有任意 goroutine 处于 runnable 状态,运行时就不会报死锁,反而掩盖问题 - 用
runtime.NumGoroutine()判断数量是否归 1:goroutine 数量 ≠ 执行状态,可能早已阻塞但未被调度器标记为asleep - 把
sync.WaitGroup当值传递进 goroutine:导致wg.Done()在副本上执行,wg.Wait()永远不返回
竞态条件必须用 go run -race 插桩捕获
go run -race 是唯一能可靠发现数据竞争的手段。它通过编译期插桩监控所有内存读写,一旦两个 goroutine 在无同步下对同一地址做“一写一读”或“两写”,立刻报错。
注意它不查死锁,但很多“假死锁”其实是竞态导致的逻辑断链。例如:
- 两个 goroutine 同时修改共享
map,其中一个覆盖了控制字段(如done标志),另一个永远收不到通知,卡在select {}或 channel 接收上 - 未加锁的计数器累加,结果不稳定,但程序不 panic,容易误判为性能问题而非并发 bug
示例中 counter++ 看似一行,实则拆为读-改-写三步,多个 goroutine 可能同时读到旧值,各自加 1 后写回,最终只 +1 而非 +2。
/debug/pprof/block 是死锁前最准的现场快照
当程序“变慢”“没响应”但还没 panic,/debug/pprof/block 能暴露真实阻塞点。它只显示因同步原语(channel、mu.Lock()、semaphore)而阻塞的调用链,含等待时长和 goroutine ID。
启用方式极简:
- 导入:
import _ "net/http/pprof" - 启动 HTTP 服务:
http.ListenAndServe(":8080", nil) - 访问:
http://localhost:8080/debug/pprof/block
关键观察点:
- 某
ch 或 <code> 操作等待超 3 秒 → 基本确认接收端缺失、未启动或已退出 - 多个 goroutine 都停在同一个
mu.Lock()调用,且堆栈里找不到对应mu.Unlock()→ 锁未释放或重复加锁
嵌套锁顺序不一致是死锁最高发场景
两个 goroutine 分别以不同顺序获取同一组 mutex,比如 goroutine A 先 mu1.Lock() 再 mu2.Lock(),goroutine B 先 mu2.Lock() 再 mu1.Lock(),必然死锁。
解决方案只有一个:固定获取顺序。可按 mutex 变量名字符串排序,或更稳妥地按内存地址排序(Go 中 &mu1 是确定的):
- 所有 goroutine 必须先获取地址小的锁,再获取地址大的锁
- 避免在锁内调用可能获取其他锁的函数,防止隐式嵌套
- 若必须多锁,封装成原子函数,内部统一顺序,对外只暴露单次调用入口
复杂点在于,这种顺序依赖往往藏在深层调用链里,靠人工 review 很难发现,必须结合 /debug/pprof/block 的 goroutine ID 和调用栈交叉比对。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











