应仅在读远多于写(如读:写≥10:1)、读操作不修改数据时使用sync.RWMutex;典型场景为配置缓存、白名单映射、路由快照;反例是高频计数器或实时指标,此时用Mutex更优。

读写锁不是万能的,用错场景反而比 sync.Mutex 更慢,甚至引发写饥饿或读饥饿。
什么时候该用 sync.RWMutex 而不是 sync.Mutex
核心判断标准只有一条:共享数据是否「读多写少」——读操作频率远高于写操作(比如读:写 ≥ 10:1),且读操作本身不修改数据。
常见符合场景:
- 配置缓存(
map[string]interface{}存配置项,启动后极少更新) - 白名单/黑名单映射表(定时同步,但查询极频繁)
- HTTP 服务中的只读元数据(如路由树快照、服务发现节点列表)
反例:高频更新的计数器、实时聚合指标、消息队列消费位点——这些写操作密集,用 RWMutex 会因写锁阻塞所有读,吞吐反而下降。
立即学习“go语言免费学习笔记(深入)”;
RLock() 和 Lock() 的调用顺序不能颠倒
如果你在持有读锁期间尝试获取写锁,会死锁。Go 不允许读写锁升级(即 RLock() → Lock())。
典型错误模式:
rw.RLock() defer rw.RUnlock() // ... 读完发现要改,想直接 Lock rw.Lock() // ❌ 死锁!当前 goroutine 已持读锁,Lock 会等所有读锁释放
正确做法是:先释放读锁,再申请写锁;或者一开始就用写锁(如果逻辑上无法预判是否要写)。
更安全的写法:
- 读操作明确只读 → 用
RLock() - 读+条件写 → 先用
RLock()快速判断,RUnlock()后再Lock(),重读并写入 - 不确定是否要写 → 直接上
Lock(),避免两次加锁开销和逻辑绕弯
写优先策略下,RWMutex 可能饿死写操作
Go 的 sync.RWMutex 默认是「读优先」,但它的实际调度策略是「读写交替」:当有写操作在排队时,新来的读请求会被阻塞,让写操作尽快执行;但如果写操作刚释放,立刻涌进大量读请求,又会把下一个写操作卡住。
现象:
- 日志里看到写操作延迟突增(>100ms)
- 压测时写吞吐上不去,CPU 却不高
-
go tool trace显示大量 goroutine 在runtime.semacquire上等待
缓解方法:
- 控制读请求并发量(例如加限流中间件)
- 对写操作做批量合并(如把 10 次小写合并为 1 次 map 更新)
- 真写密集场景,别硬扛 —— 改用
sync.Mutex或无锁结构(如sync.Map,但注意它不保证遍历一致性)
嵌套锁和 defer 使用必须严格配对
RLock() 必须配 RUnlock(),Lock() 必须配 Unlock(),混用或漏写会导致锁永远不释放。
常见坑:
- 在
RLock()后写了return,但忘了RUnlock()—— 后续所有写操作永久阻塞 - 用
defer rw.RUnlock()但函数提前 panic,defer 不执行(其实会执行,但很多人误以为不会) - 在循环里反复
RLock()却只RUnlock()一次(RWMutex不支持重入)
最稳妥写法始终是:
rw.RLock() defer rw.RUnlock() // 中间任意 return / panic 都安全
注意:defer 是函数返回时执行,不是 panic 时跳过 —— 这点很多人记反。
真正难的不是怎么加锁,而是判断「这里到底要不要锁」以及「该用哪种锁粒度」。一个 map 字段被 3 个函数读、1 个函数写,不代表全得套 RWMutex —— 有时把写操作抽成原子更新函数 + channel 控制,比锁更干净。


















