go-deadlock 默认不监控 sync.rwmutex 读锁重入和锁升级,因其仅包装 sync.mutex;必须显式启用 rlock: true 才能检测 rwmutex 相关违规,否则无法发现此类隐式死锁。
go-deadlock 能排查网络服务里的锁死锁,但默认对 sync.rwmutex 读锁重入和锁升级场景完全静默——必须显式启用 rlock: true,否则根本看不到问题。
为什么默认不报 sync.RWMutex 读锁重入
标准库明确禁止同一个 goroutine 多次调用 mu.RLock(),或在已持读锁时调用 mu.Lock()。但 go-deadlock 默认只包装 sync.Mutex,所有 sync.RWMutex 实例都不受监控。结果就是:程序逻辑上已破坏锁约束,pprof 显示大量 goroutine 卡在 (*RWMutex).RLock 或 (*RWMutex).Lock,而 go-deadlock 一声不吭。
实操建议:
- 初始化时必须传完整选项:
deadlock.Opts{RLock: true, ReportAll: true} - 仅在测试/开发环境启用;生产环境禁用(它会跟踪每个 goroutine 的锁状态,带来可观开销)
- 若第三方库内部用了
RWMutex导致兼容问题,改用白名单模式:只包装你自己的结构体字段,不全局替换
网络服务中典型的锁升级死锁链
HTTP handler 先 mu.RLock() 读缓存,再异步触发更新(比如调下游 API 后写回),而更新逻辑里直接 mu.Lock() —— 此时 goroutine 已持读锁,mu.Lock() 会永久阻塞。其他 goroutine 全部卡在 mu.RLock() 等待读权限,因为 RWMutex 内部计数器已被污染。
关键特征:
- panic 日志里没有
fatal error: all goroutines are asleep - deadlock! - pprof 查看
/debug/pprof/goroutine?debug=2,发现大量 goroutine 停在(*RWMutex).Lock或(*RWMutex).RLock -
runtime.NumGoroutine()持续上涨,但无实际请求处理进展
怎么修复锁升级逻辑
绝不在已持有 mu.RLock() 的 goroutine 中调用 mu.Lock()。这不是粒度问题,是使用规则 violation。
可行方案:
- 先
mu.RUnlock(),再mu.Lock()(注意:中间状态需重新校验,防止竞态) - 把“读-判断-写”拆成两阶段:第一阶段只读 + 生成变更计划(如 struct{} 或 map[string]interface{}),第二阶段起新 goroutine 拿写锁执行
- 用
atomic.Value或 channel 传递变更计划,避免共享锁 - 用
go tool trace查看 goroutine 状态变迁,重点找SyncBlock → Runnable → SyncBlock循环,这是锁升级失败的铁证
为什么 -race 完全抓不到这类问题
-race 检的是数据竞争(多个 goroutine 无同步地读写同一地址),而读锁重入、锁升级失败属于逻辑死锁:所有内存访问都加了锁,线程安全,只是违反了 RWMutex 的调用契约。运行时无法从指令流推断“这里不该再 RLock”,所以 -race 静默。
真正容易被忽略的点是:这类死锁不会让程序立即 panic,而是表现为缓慢劣化——延迟飙升、goroutine 数堆积、CPU 却不高。等到 pprof 显出异常时,问题往往已在线上跑了好几天。











