rwmutex仅在读占比≥99%且写极少时不频繁时才优于mutex;写稍频繁即更慢、易panic,典型适用场景为缓存、配置快照等,反例包括计数器、高频状态机。

sync.RWMutex 只在「读远多于写」时才比 sync.Mutex 快;写操作稍一频繁(比如每秒几十次),它反而更慢,且容易因配对错误直接 panic。
什么时候该用 RWMutex 而不是 Mutex
核心判断标准就一条:你这个共享数据结构是否满足「99% 以上操作是读,写极少且不密集」。
- 典型场景:
map[string]*User缓存、配置项快照、路由表、静态资源元信息索引 - 反例场景:计数器(哪怕读多写少)、高频更新的状态机、日志缓冲区 —— 这些写锁竞争一上来,
RWMutex的读计数和唤醒开销就拖垮性能 - 别被“读多”误导:如果写操作集中在某几秒爆发(比如配置热重载+批量刷新),
RWMutex的写优先策略会让后续所有RLock()排队,读延迟飙升
RUnlock() panic:“sync: RUnlock of unlocked RWMutex” 怎么定位
这不是“没加锁”,而是“多解了一次锁”或“在没加锁的路径上误调了 RUnlock()”。
- 最常见写法错误:
RLock()在if分支里,但defer RUnlock()写在了外层 —— 某些分支根本没进RLock(),却执行了RUnlock() - error early return 是高危区:必须确保每个
return前都已RUnlock(),或统一用defer但只在真正加锁后注册 - 别在循环体里反复
RLock()/RUnlock():粒度太细,易漏配对,也放大开销 - 用
go build -race运行,race detector 能直接标出不匹配的锁操作位置
嵌入结构体时,RWMutex 字段必须用指针接收器
如果你把 sync.RWMutex 当作结构体字段,又写了值接收器方法,那锁完全无效。
- 错例:
func (s MyConfig) Get() string { s.mu.RLock(); defer s.mu.RUnlock(); return s.data }——s.mu是副本,加锁白加 - 正解:
func (s *MyConfig) Get() string { s.mu.RLock(); defer s.mu.RUnlock(); return s.data } -
RWMutex是零值可用类型,无需显式初始化,但嵌入后必须通过指针访问才能生效
写锁阻塞新读锁,但已持有的读锁不中断
RWMutex 是写优先设计:一旦有 goroutine 调用 Lock(),后续所有 RLock() 都会排队,直到写锁释放;但正在执行的读操作不受影响。
- 好处:防写饥饿 —— 写请求不会被无限期卡住
- 代价:读请求堆积,尤其在写操作耗时长时,
RLock()可能等数秒 - 不能在持有读锁时调用
Lock()(死锁),也不能在持有写锁时调用RLock()(语法允许但逻辑混乱) - 若需读-写切换,必须先
RUnlock(),再Lock(),中间存在极短窗口期,业务需自行处理竞态
Mutex 那样公平排队,而是在写请求到达那一刻起,就把新来的读全部拦在外面。这点在做 SLA 敏感服务时,必须压测验证。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











