sync.Map 仅适用于读远多于写(如95%+)、键数量稳定、无需遍历或TTL的场景;盲目使用会拖慢写入、增加内存与GC压力。
sync.map 不是通用缓存方案,只在读远多于写(比如 95%+ 读)、键数量稳定、且不需遍历或 ttl 的场景下才比 map+sync.rwmutex 快;盲目替换反而拖慢写入、涨内存、卡 gc。
什么时候该用 sync.Map 而不是加锁的 map
它只赢在「大量 goroutine 同时读、极少 goroutine 写」这个狭窄窗口里。典型例子:服务启动时加载几百个配置项,之后只读不改;或者请求上下文里缓存临时元数据(如 traceID → span 对象),生命周期短、key 不重复、不遍历。
- 读操作无锁,100 万次并发读实测比加锁
map快 2–3 倍 - 写操作会触发 dirty map 升级和复制,每秒几百次
Store就可能性能断崖下跌 - 不支持
len()、不能遍历一致性、无法原子清空——想批量删或统计 size 得自己维护额外字段 - 内部 read/dirty 双结构导致
Delete后再Store同一键,可能仍从 read map 命中旧值
LoadOrStore 不是原子初始化银弹
它看着方便,但高并发下容易成为热点:key 不存在时会升级 dirty map 并复制整个结构。实际业务中,更稳的做法是先 Load,未命中再单独 Store。
- 用
LoadOrStore的前提是:你接受它在写路径上引入额外开销,且 key 初始化后几乎不更新 - 如果 value 构造成本高(比如要查 DB 或解密),别在
LoadOrStore回调里做——它可能被多次执行 - 永远别依赖它的返回值判断“是否首次插入”来控制逻辑,因为并发下语义不可靠
别指望 sync.Map 自带过期或淘汰
它没有 expire 字段,也不清理已删 key。所谓“过期”,只能靠你在 value 里手动存时间戳,每次 Load 后检查并主动 Delete;但这样既拖慢读,又留不住真正过期的 key(因为 Delete 不保证立即从 read map 移除)。
- 需要 TTL?直接换
github.com/bluele/gcache,它默认用map+sync.RWMutex,支持 LRU/expire/maxSize,不到 300 行 - 要用
sync.Map硬扛,就得接受“只存生命周期明确、不更新、不淘汰”的数据,比如 userCache 按 ID 缓存几百个用户基本信息 - value 是大对象(如
[]byte或嵌套 struct)时,记得Delete后显式置空引用,否则 GC 不回收
真正难的不是选 sync.Map 还是 map,而是把「读多写少」这个前提落到具体业务行为上——比如配置热更新接口每分钟最多触发一次写,而读请求每秒几千;一旦写频次上去了,sync.Map 就从加速器变成瓶颈源。


















