
在 go 中,当多个 goroutine 同时访问同一变量(尤其是 map)时,若存在任何写操作,所有读写均需同步保护;仅读操作看似安全,实则仍可能引发 panic 或数据竞争,必须使用 sync.rwmutex 等机制协调。
在 go 中,当多个 goroutine 同时访问同一变量(尤其是 map)时,若存在任何写操作,所有读写均需同步保护;仅读操作看似安全,实则仍可能引发 panic 或数据竞争,必须使用 sync.rwmutex 等机制协调。
Go 的内存模型明确规定:对同一变量的并发读写(其中至少一次是写操作)属于未定义行为(undefined behavior),必须通过同步原语显式协调。这不仅关乎数据一致性,更直接影响程序稳定性——例如对 map 的并发读写会直接触发运行时 panic(fatal error: concurrent map read and map write),而非静默错误。
以你提供的 MemoryCache 为例,当前实现存在严重竞态风险:
// ❌ 危险:Get 方法未加读锁!
func (m *MemoryCache) Get(key string) interface{} {
if v, ok := m.c[key]; ok { // 并发读 + 其他 goroutine 正在 Put → panic!
return v
}
return nil
}
即使 Get 仅执行读取,只要 Put(写入)可能同时发生,就必须加锁。sync.RWMutex 是最佳选择:它允许多个 goroutine 并发读(RLock/RUnlock),但写操作会独占锁(Lock/Unlock),兼顾性能与安全性。
✅ 正确实现应统一加锁:
// ✅ 安全:读写均受 RWMutex 保护
func (m *MemoryCache) Get(key string) interface{} {
m.rw.RLock() // 获取读锁
defer m.rw.RUnlock()
if v, ok := m.c[key]; ok {
return v.value // 注意:返回 value 字段,非整个 *MemoryCacheValue
}
return nil
}
func (m *MemoryCache) Put(key string, expires int64, value interface{}) {
m.rw.Lock() // 获取写锁
defer m.rw.Unlock()
m.c[key] = &MemoryCacheValue{
value: value,
expires: time.Now().Unix() + expires,
}
}
⚠️ 关键注意事项:
- 读锁也必须 defer 解锁:遗漏 RUnlock() 将导致后续所有读写永久阻塞。
- 避免锁内执行耗时操作:如网络请求、文件 I/O,应先获取数据再解锁处理。
- 不要复制 map 后解锁再读:for k, v := range m.c 在循环中仍处于临界区,需全程持锁;若需深拷贝,应在锁内完成。
- 考虑替代方案:对高读低写场景,可结合 sync.Map(专为并发优化的 map,但不支持遍历)或 channel 模式(如示例中的控制器 goroutine),但 RWMutex + map 仍是通用性与性能平衡的最佳实践。
总结:Go 中没有“只读安全”的侥幸空间。只要共享数据存在被写的可能,所有访问(无论读或写)都必须同步。这是编写健壮并发程序的铁律,也是 go run -race 工具重点检测的核心竞态模式。











