
go 中的 map 本身不支持并发读写,若多个 goroutine 同时读写同一 map,会导致 panic;但并发只读是安全的。因此遍历 map 前必须加读锁(rlock),而删除或添加元素时需加写锁(lock),且不能在 range 过程中直接修改 map。
go 中的 map 本身不支持并发读写,若多个 goroutine 同时读写同一 map,会导致 panic;但并发只读是安全的。因此遍历 map 前必须加读锁(rlock),而删除或添加元素时需加写锁(lock),且不能在 range 过程中直接修改 map。
在你的 notifyAll 场景中,必须对 map 加 RLock 后再 range——否则即使只是读取,一旦其他 goroutine 正在执行 addConn 或 removeConn(即写操作),就会触发运行时 panic:“fatal error: concurrent map read and map write”。
你原代码中的问题不止一处:
- ❌ range self.connections 未加锁,存在并发读写风险;
- ❌ 在 range 循环体内启动 goroutine,并在其中调用 self.removeConn(k) —— 而 removeConn 内部使用 mu.Lock() 删除 map 元素,这会导致正在被 range 的 map 被并发修改,Go 运行时会立即崩溃;
- ❌ go func(int k, conn *Conn) { ... }(k, v) 捕获的是循环变量 k 和 v 的地址(而非值),若未显式传参或闭包捕获,易引发变量覆盖 bug(虽你已传参,但结构易误)。
✅ 正确做法是:
- 读前加 RLock,读完立即 RUnlock(注意:不能 defer 在函数开头,否则锁会持续到函数结束,阻塞写操作);
- 将需操作的 key/value 快照复制到局部切片或通道中,再在 goroutine 中处理;
- 删除操作必须通过写锁串行化,且绝不在 range 过程中直接删 map。
以下是修正后的推荐实现:
func (s *Server) notifyAll(event *Event) {
// Step 1: 安全快照 —— RLock + 复制键值
s.mu.RLock()
conns := make([]*Conn, 0, len(s.connections))
keys := make([]int, 0, len(s.connections))
for k, v := range s.connections {
keys = append(keys, k)
conns = append(conns, v)
}
s.mu.RUnlock()
// Step 2: 并发发送(不持有锁)
ch := make(chan struct{}, 64)
for i, conn := range conns {
ch <p>⚠️ 注意事项:</p>
- 不要使用 defer mu.RUnlock() 在函数入口处——它会使读锁持续整个函数生命周期,严重降低并发性能;
- range 本身不保证顺序,且每次迭代顺序随机(这是设计特性,非 bug),但不影响正确性;
- 若 connections 频繁增删,且 notifyAll 调用密集,建议采用 channel-based 架构(如答案中提供的 agent 模式),彻底规避锁竞争,提升可维护性与伸缩性;
- sync.Map 仅适用于键值类型固定、读多写少、且无需遍历全部元素的场景;它不支持 range,也无法满足你“遍历 + 条件删除”的需求,故此处不适用。
总结:并发 map 访问的黄金法则——读用 RLock,写用 Lock,绝不混合;range 前必加读锁,修改 map 务必串行化;复杂逻辑优先考虑基于 channel 的无锁协作模型。











