在 Go 中,map 默认情况下 不是线程安全 的数据结构。来自不同 goroutine 的同时写入或修改 map 会导致竞态条件和 "concurrent map writes" 类型的恐慌。通常使用 sync.Mutex、sync.RWMutex 或标准库中的特殊类型 sync.Map 来保护 map,该类型实现了安全的原子操作。
可以通过 delete(map, key) 删除 map 中的元素,但是在通过 range 迭代 map 时会有一些细节:
线程安全地操作 map 的示例:
type SafeMap struct { mu sync.RWMutex m map[string]int } func (s *SafeMap) Load(key string) (int, bool) { s.mu.RLock() v, ok := s.m[key] s.mu.RUnlock() return v, ok } func (s *SafeMap) Store(key string, value int) { s.mu.Lock() s.m[key] = value s.mu.Unlock() }
问题:在对同一个 map 进行 range 迭代时,可以安全地删除 map 中的元素吗?
答案: 可以,只要删除的键是在 range 迭代中已经发出的键。但在此过程中不能同时从其他 goroutine 修改 map:这将导致竞态条件。
m := map[string]int{"a": 1, "b": 2, "c": 3} for k := range m { delete(m, k) // 安全!(如果仅在此 goroutine 中进行) }
故事
一个监控服务在 map 上进行统计,并且从多个 goroutine 同时更新(指标计数器)。在高峰时刻发生了 "concurrent map writes" 的恐慌,服务中断,数据丢失。解决方案:添加互斥锁或使用 sync.Map 代替普通的 map。
故事
在数据迁移过程中,有人决定通过并行 goroutine 加快大 map 的清理,每个 goroutine 删除其部分键通过 range。结果是常见的数据竞态和不可预测的崩溃。迁移后必须回到顺序清理或在 range 期间阻止访问。
故事
在对 map 的 range 循环中,开发者向同一个 map 中添加新数据(例如,生成图的相邻节点列表)。结果发现,新键可能在当前 range 迭代中被忽略,导致图的处理不完整。此 bug 仅在对少见案例进行全面测试时被发现。修复后,该算法被重写为使用单独的添加队列。