go map 多协程读写会 panic,因非并发安全;sync.map 适用于读多写少场景,否则应选 sync.rwmutex;即使只读也需锁,因扩容等操作引发竞争。

Go map 在多协程中直接读写会 panic
Go 的内置 map 不是并发安全的。只要有一个 goroutine 在写,其他 goroutine 无论读还是写,都可能触发 fatal error: concurrent map read and map write。这不是概率问题,而是运行时检测到冲突后主动崩溃——Go 故意不隐藏这个问题,逼你正视它。
常见错误现象:
- 程序偶发 panic,日志里只有
concurrent map read and map write,但复现困难 - 用
sync.Map替换后不 panic 了,但性能反而更差(误用场景) - 加了
sync.RWMutex,但只锁了写操作,读没锁,照样 panic
什么时候必须用 sync.RWMutex 而不是 sync.Map
sync.Map 是为「读多写少 + key 生命周期长」设计的,内部做了分段、延迟初始化、只读副本等优化。但它不支持遍历、不支持获取长度、不能保证迭代一致性,且写入新 key 的首次成本较高。
如果你的场景满足以下任意一条,sync.RWMutex 更合适:
立即学习“go语言免费学习笔记(深入)”;
- 需要频繁调用
len(m)或遍历所有 key(比如做 metrics 汇总) - key 会高频增删(比如 session 管理,连接建立/断开频繁)
- 读写比例并不悬殊(比如读写比在 5:1 以内)
- 需要原子性地执行「读-改-写」(如
map[key]++),sync.Map没有对应原子操作
示例:用 sync.RWMutex 安全读写
var mu sync.RWMutex var m = make(map[string]int) // 读 mu.RLock() v := m["key"] mu.RUnlock() // 写 mu.Lock() m["key"] = 42 mu.Unlock()
sync.Map 的典型误用:当普通 map 用
sync.Map 的 API 和普通 map 不兼容:没有 range,没有 delete 函数,值类型必须是 interface{},且每次 Load/Store 都有类型断言或接口转换开销。
容易踩的坑:
- 把
sync.Map当成「线程安全版 map[string]int」直接用,结果发现for k, v := range m报错——sync.Map不支持 range - 反复调用
m.Load("key")得到interface{},又忘记类型断言,导致 panic 或逻辑错误 - 在 hot path 上用
sync.Map.LoadOrStore处理默认值,但其实 key 绝大多数时候已存在,此时LoadOrStore比单纯Load多一次原子判断,白费开销
为什么不能靠“只读不写”就省掉锁
即使你确定某个 goroutine 只读、另一个只写,Go 运行时仍无法静态判断这种分工,只要底层指针被多个 goroutine 同时触达,就可能触发内存模型层面的竞争。更关键的是:map 底层结构在扩容时会整体迁移数据,此时任何并发读都会看到中间态(比如 bucket 为空、hmap.flags 被修改),直接 crash。
所以不存在“我保证只读,所以不用锁”这种侥幸。唯一例外是 map 初始化后**彻底冻结**(不再写入),且所有读操作发生在写入完成后——这时可以用 sync.Once 配合普通 map,但必须确保写入完成与读取开始之间有明确 happens-before 关系。
复杂点在于:map 并发不安全不是 bug,是 Go 明确的设计取舍。它把成本和责任交给你,而不是用默认锁拖慢所有使用场景。选对同步原语的前提,是看清你的访问模式到底是什么样的。










