sync.Map 并非万能,90% 场景下 sync.RWMutex + 原生 map 更快更可控;LoadOrStore 在 Delete 后不恢复 key 因其仅在 key 完全不存在时写入;Range 是全量快照而非迭代器;适用前提为 key 集合稳定、value 更新间隔长;高频写或短生命周期 key 反致性能下降。
别急着用 sync.map,它不是线程安全版的 map,也不是万能解药;90% 的并发字典场景,sync.rwmutex + 原生 map 更快、更可控、更易维护。
为什么 LoadOrStore 有时加不回被删掉的 key
这是最常踩的坑:调用过 Delete 后,再用 LoadOrStore 发现 key 始终不出现。原因在于 LoadOrStore 只在 key “完全不存在”时才写入;而 Delete 实际只是把对应 entry.p 标记为 nil(逻辑删除),key 仍留在 read.m 中——此时 LoadOrStore 认为“存在”,直接返回 nil, false,并不重建。
正确做法是:
- 需要“确保 key 存在并获取值”,用
Load+Store组合,显式控制流程 - 仅当明确要“原子性初始化一次(且永不覆盖)”,才用
LoadOrStore - 若必须恢复已删 key,改用
Store,它会强制写入dirty并在下次Load时同步到read
Range 不是迭代器,是全量快照
Range 看起来像遍历,实则内部会:
- 先锁住整个
sync.Map(mu) - 复制当前
read.m和dirty中所有有效entry到临时 slice - 解锁后逐个回调
f(key, value)
这意味着:
- 你在回调里调
Delete或Store,对本次Range看到的数据无影响 - 无法中途退出(
f返回false仅终止后续回调,不中断复制过程) - 数据可能已过期:复制完成后,其他 goroutine 可能已修改或删除了某些 key
真要遍历+过滤,优先考虑定期 dump 到普通 map 再处理,而不是依赖 Range。
读多写少 ≠ 所有读多写少都该用 sync.Map
sync.Map 的“读多写少”有严格前提:
- key 集合基本稳定(新增极少,几乎不删)
- 单个 key 的 value 被高频读取,但更新间隔长(秒级或更久)
- 写入操作集中在初始化或配置热加载阶段
典型适用场景:
- HTTP handler 中缓存长期有效的用户 session(只增不删)
- 服务启动后只读的路由表、配置项映射
- 指标聚合中固定 metric name → counter 的映射
反例:
- 高频更新的计数器(如每毫秒
Store一个递增值)→sync.Map晋升开销大,不如sync.RWMutex+map[int64]int64 - 短生命周期 key(如请求级上下文缓存)→
misses快速触发晋升,dirty频繁重建,性能反低于加锁 map
性能对比:Load vs LoadOrStore vs mutex + map
基准测试(Go 1.21,Intel i7,100 个 goroutine)显示:
- 纯读场景(
Load占 95%):sync.Map比sync.RWMutex+map快约 2.3×(无锁读优势明显) - 读写混合(
Load70%,Store30%):sync.RWMutex+map反而快 1.8×(sync.Map晋升和dirty同步开销凸显) - 高频
LoadOrStore(key 已存在):比纯Load多一次原子比较,吞吐下降约 12%
结论很实在:压测前别猜,用 go test -bench=. 对比两种实现。尤其当写入比例 >10%,sync.Map 很可能拖慢你。
真正容易被忽略的是:sync.Map 的 misses 计数器和晋升机制不是黑盒——它会默默改变行为。一旦 read 命中率跌破阈值(默认 0),就会把整个 dirty 提升为新 read,并清空 dirty。这个过程要锁 mu,且复制成本随 dirty size 线性增长。如果你没监控 misses,就等于在盲开高速路。



















