709 字
4 分钟
sync.Map 不是并发 map 的银弹

看到“并发安全的 map”,很多人的第一反应是 sync.Map。但它的文档写得很清楚:它是为读多写少、键集合基本稳定的场景优化的,不是 map 加锁的通用替代品。

内部是两张表#

sync.Map 里有一个只读的 read 和一个可写的 dirty。读操作走 read,大部分时候只需要一次原子加载;写入发现键不在 read 里,才会加锁去 dirty 里找或插入。

当 dirty 的写入积累到一定程度,它会被提升为新的 read,旧的 read 里被删除的键这时才真正消失。这解释了它的几个行为:

  • 读路径几乎没有锁,所以读多写少时很快;
  • 写入要维护两张表,写多时不如普通 map 加锁;
  • 已删除的键在被提升之前仍然占着位置,键空间反复变化时内存不会立刻下降。

它没有的东西#

var m sync.Map
m.Store("a", 1)
v, ok := m.Load("a")
m.Range(func(k, v any) bool {
// 遍历不保证是某一时刻的一致快照
return true
})
  • 没有 len:想知道大小只能 Range 数一遍,而 Range 期间可能有并发写入。
  • 没有类型安全:键值都是 any,每次存取都有装箱和类型断言。
  • 没有原子批量操作:需要“读-改-写”时,仍然要自己用 LoadOrStore 或 CompareAndSwap 拼逻辑。
  • Range 不是快照:它不阻塞写入,遍历结果可能包含已经被删除的键,也可能漏掉遍历开始后才插入的键。

什么时候值得用#

典型场景是“进程级缓存,键在启动或首次访问时写入,之后几乎只读”:

var routes sync.Map // path -> handler,启动期写入,运行期只读
func lookup(path string) (Handler, bool) {
v, ok := routes.Load(path)
if !ok {
return nil, false
}
return v.(Handler), true
}

反过来,如果键会频繁增删,或者需要遍历、计数、批量更新,sync.RWMutex + map 通常更简单也更快。另一个常见选择是分片锁:把键哈希到 N 个桶,每个桶一把锁,冲突概率随 N 下降。

type sharded[K comparable, V any] struct {
shards [16]struct {
mu sync.RWMutex
m map[K]V
}
}

一个容易忽略的坑#

sync.Map 的零值可用,但不要复制它。它内部包含互斥锁和状态字段,复制之后两份状态各自独立,行为会变得不可预测。作为结构体字段时,用指针或者确保结构体本身不被复制。

结论#

sync.Map 是一个针对特定访问模式的优化结构,不是“并发 map”的同义词。选它之前先确认:键集合稳定吗?读远多于写吗?需要长度和一致性遍历吗?三个问题里有一个答不上来,普通 map 加锁往往更合适。

sync.Map 不是并发 map 的银弹
/posts/go-sync-map/
作者
fantasy-peak
发布于
2026-08-14
许可协议
CC BY-NC-SA 4.0