Go运行时通过检查hmap.flags中的hashWriting标志位检测并发写:写操作前置位,若已置位则panic;读操作因可能遭遇写中间态(如扩容、指针未更新)也会panic;sync.Map绕过该机制,用锁保护dirty字段;RWMutex漏锁常见于range未全覆盖、嵌套map未锁内层、值接收者导致锁失效等。

Go 运行时如何检测 concurrent map writes
panic 不是靠锁竞争或内存地址冲突“偶然发现”的,而是 runtime 在每次写操作入口主动检查一个标志位:hashWriting。这个标志存在 hmap.flags 字段中,写操作开始前会执行 flags |= hashWriting,结束后再清掉。如果此时发现 flags & hashWriting != 0,说明另一个 goroutine 已经在写了,立刻触发 fatal error: concurrent map writes。
为什么读操作也会 panic:concurrent map read and map write
读操作(包括 for range m、v, ok := m[k])本身不改 hashWriting,但可能撞上写操作的中间态:
- 桶数组正在扩容,旧桶已部分迁移,新桶尚未就绪
- 某个键值对指针被写入一半(如只更新了 key,value 还是野指针)
- 迭代器正遍历某个桶,而该桶被另一个 goroutine 的
delete触发了溢出链表裁剪
runtime 会在读路径中检查是否处于写状态(例如通过 hmap.oldbuckets != nil 或桶指针有效性),一旦判定读-写交叉,就抛出 fatal error: concurrent map read and map write。
sync.Map 为什么没用 hashWriting 检测
sync.Map 完全绕开了原生 map 的运行时检测机制,因为它根本不直接操作 hmap 结构:
- 它的
read字段是atomic.Value,存储的是只读的readOnly结构,无写入逻辑 - 它的
dirty字段才是真实 map,但所有对 dirty 的访问都受mu(互斥锁)保护 - 没有 goroutine 能绕过锁直接调用底层 map 的写函数,所以
hashWriting根本不会被并发置位
换句话说:sync.Map 的安全不是因为“检测更松”,而是因为它从源头上杜绝了并发进入原生 map 写路径的机会。
加了 sync.RWMutex 却还是 panic?常见漏点
锁了但没锁全,是最隐蔽也最常发生的错误:
-
range m必须在RLock()期间完成,且不能在循环体里调用delete或赋值——哪怕只读,只要写操作在别处发生,就可能被 runtime 捕获 - 嵌套 map 场景下,只锁了外层,内层 map 的写仍裸奔(如
m["a"]["b"] = 1,m["a"]是另一个 map) - 方法接收者是值类型,导致每次调用都拿到 map 的副本,锁失效(应使用指针接收者)
- 忘记在 defer 前 unlock,或 panic 后未 recover 导致锁未释放,后续 goroutine 阻塞后超时重试,误以为是死锁而非 map panic
真正难的从来不是“加锁”,而是确保所有通往该 map 的代码路径——无论深几层、跨几个函数、是否在 defer 或 goroutine 中——都落在同一把锁的保护范围内。


















