<p>go-deadlock 不检测运行时级死锁,只提前发现潜在锁等待循环;它通过监控 deadlock.Mutex.Lock() 超时(默认30秒)主动 panic,而非等待所有 goroutine 休眠,故能比 fatal error: all goroutines are asleep - deadlock! 更早暴露 AB-BA 等问题。</p>

go-deadlock 不检测 Go 运行时级死锁(如 fatal error: all goroutines are asleep - deadlock!),它只在运行时提前发现潜在的、尚未卡死的锁等待循环。
为什么 go-deadlock panic 不等于 runtime 死锁
Go 运行时触发 fatal error: all goroutines are asleep - deadlock! 的条件非常严格:所有 goroutine 必须同时处于阻塞态,且无任何 runnable 状态。而 go-deadlock 的机制完全不同——它不等程序卡死,而是监控每个 deadlock.Mutex.Lock() 调用的等待时间。一旦某个 goroutine 在 Lock() 上阻塞超过 DeadlockTimeout(默认 30 秒),就主动 panic 并打印调用链。
这意味着:
- 它能比 runtime 更早暴露问题,比如 AB-BA 锁顺序冲突刚发生、还没蔓延到全系统卡住时就报出
- 它完全不感知 channel 阻塞、
select{}、time.Sleep或未关闭的for range——这些导致的 runtime 死锁,go-deadlock一概不管 - panic 日志里出现
potential deadlock,说明是锁等待超时,不是“已经死锁”,但路径已真实复现
必须全局替换 sync.Mutex 才生效
go-deadlock 是运行时 hook,不是静态分析器。它靠重写 Lock()/Unlock() 方法内部逻辑来记录锁持有关系和等待图。混用会彻底失效:
- 所有生产代码中声明为
sync.Mutex的变量,都得显式改成deadlock.Mutex(或通过构建标签切换的接口) - 不能只在测试文件里临时 new 一个
deadlock.Mutex去测某段逻辑——其他 goroutine 还在用原生sync.Mutex,检测链就断了 - 第三方库内部使用的
sync.Mutex不会被捕获;若需覆盖,得改其源码或用go:replace替换整个sync包(极不推荐)
检测 sync.RWMutex 读锁重入要手动开开关
标准库明确禁止同一个 goroutine 多次调用 RLock(),但 go-deadlock 默认不检查这个行为。不启用的话,这种错误会静默发生,最终让其他 goroutine 卡在 Lock() 或后续 RLock() 上。
正确做法是初始化时显式配置:
deadlock.Opts = deadlock.Opts{
RLock: true,
ReportAll: true,
}注意:
- 启用后,所有
sync.RWMutex实例都会被包装,包括你没直接控制的第三方库锁 - 如果遇到兼容问题(比如某些库依赖
sync.RWMutex的底层字段布局),可改用白名单模式:仅对自定义结构体中的字段做类型替换 - 锁升级场景(先
RLock()再Lock())仍是高危区,go-deadlock只能报等待超时,无法判断“该不该升级”——这得靠代码逻辑修正
测试中误报多?先看是不是测试写法错了
CI 里跑 go test -tags=testdeadlock 突然 panic,90% 不是真死锁,而是测试本身没写对:
- 主 goroutine 用
time.Sleep(1 * time.Second)等待子 goroutine,但子 goroutine 其实早就 panic 退出了 → 主 goroutine 继续 sleep,deadlock.Mutex等锁超时 panic -
defer mu.Unlock()放在mu.Lock()后,但中间立刻 panic →Unlock()被跳过,锁没释放,下一个测试用例直接卡住 - panic 日志显示两个 goroutine 分别停在
mu1.Lock()和mu2.Lock(),且堆栈里它们之前已分别调用过mu2.Lock()和mu1.Lock()→ 这才是真 AB-BA,必须改锁顺序
真正难缠的是那些不报 panic、但服务响应延迟飙升的 case:pprof 显示大量 goroutine 卡在 (*RWMutex).Lock,runtime.NumGoroutine() 持续涨却无处理进展——这种往往藏在 HTTP handler 的读锁升级逻辑里,go-deadlock 能帮你揪出第一处等待点,但修复还得靠拆阶段、加 channel 或用 atomic.Value 替代锁。

















