sync.RWMutex适用于读多写少场景,允许多读单写;应避免读操作误用写锁、读中触发写、锁粒度过粗、滥用原子操作及channel设计不当,优化需基于pprof分析真实瓶颈。

用 sync.RWMutex 替代 sync.Mutex 读多写少场景
当共享数据被频繁读取、极少修改时,sync.Mutex 会成为瓶颈:每次读操作都得抢锁,阻塞其他读。而 sync.RWMutex 允许多个 goroutine 同时读,仅写操作独占——这是最直接的锁粒度优化。
常见错误是误以为“读也要加写锁”,比如在 Get() 方法里调用 mu.Lock() 而非 mu.RLock();或在读操作中意外触发写(如 lazy-init 未加防护),导致死锁或 panic。
- 只在真正修改字段时用
mu.Lock()/mu.Unlock() - 所有纯读路径必须统一用
mu.RLock()/mu.RUnlock() - 避免在
RLock()持有期间调用可能写共享状态的外部函数
把锁缩小到具体字段或分片上(Sharding)
全局一把锁保护整个 map 或大结构体,是竞争根源。与其锁整个容器,不如按 key 哈希分片,每片配独立锁。Go 标准库 sync.Map 内部就用了类似思路(但它是为特殊场景设计,不建议盲目替代原生 map + 锁)。
典型误用是“为省事给整个 struct 加一把锁”,哪怕只有 10% 字段会被并发修改。这会让无关字段的访问也排队。
立即学习“go语言免费学习笔记(深入)”;
- 对高频更新的字段单独抽成子结构,配专属
sync.Mutex - map 分片示例:用
shard := hash(key) % 32映射到 32 个*sync.Mutex数组 - 注意分片数不宜过小(竞争仍高)或过大(内存/CPU 开销上升),32–256 是较稳妥起点
用无锁结构替代锁,但别过早优化
atomic 包能安全操作 int32、int64、指针等基础类型,比锁快一个数量级;sync/atomic.Value 可安全替换只读结构体(如配置快照)。但它们不适用于复合逻辑(比如“先读再判断再写”这种 CAS 循环外的场景)。
容易踩的坑是滥用 atomic.LoadPointer 去读一个未用 atomic.StorePointer 写入的地址,导致数据竞争(race detector 会报 Data race);或误以为 atomic.Value 支持任意修改——它只保证“整体赋值”原子,内部字段仍需额外同步。
- 计数器、开关标志、单次初始化指针优先用
atomic -
sync/atomic.Value适合存不可变配置、缓存对象等“整块换”的场景 - 涉及多个字段联动更新?老老实实用锁,别硬套 CAS
通过 channel 或 worker 模式把共享状态转为串行处理
有些场景本质就是不能并行修改(比如写日志缓冲区、更新指标汇总值),硬上锁只会让 goroutine 排长队。此时更优解是让所有修改请求走同一个 channel,由单个 goroutine 顺序处理——把锁竞争转为消息排队,逻辑更清晰,且避免锁误用。
问题常出在 channel 缓冲区设太小(导致发送方阻塞)或忘了关闭 worker(goroutine 泄漏);也有人把本该并发读的响应逻辑也塞进 worker,反而拖慢整体吞吐。
- worker goroutine 用
for req := range ch模式,主流程 defer close(ch) - channel 缓冲大小建议设为预期峰值 QPS × 平均处理延迟(单位秒),例如 1000 QPS × 0.01s = 10
- 只让“写状态”走 channel,读操作仍可直接访问最新快照(配合 atomic.Value 或 RWMutex)
锁优化不是越细越好,也不是越无锁越快。关键是识别真实瓶颈:先用 go tool pprof -mutex 看锁等待时间,再决定动哪一层。很多所谓“高并发问题”,其实只是某处 time.Sleep 被锁包裹,或者日志打印没关调试级别。










