在 go 中,对共享变量(如 map)进行并发读写时,若存在任何写操作,所有读写操作都必须加锁;仅读操作虽可共存,但仍需读锁保护,否则会触发竞态条件并导致程序崩溃。
在 go 中,对共享变量(如 map)进行并发读写时,若存在任何写操作,所有读写操作都必须加锁;仅读操作虽可共存,但仍需读锁保护,否则会触发竞态条件并导致程序崩溃。
Go 的 map 类型不是并发安全的——这意味着只要存在多个 goroutine 同时访问(哪怕只是读),且其中至少有一个 goroutine 在写,就构成数据竞争(race condition)。Go 运行时会在启用 -race 检测时 panic 报错,例如:
fatal error: concurrent map read and map write
因此,回到你的 MemoryCache 示例:当前实现中,Put 方法正确使用了 sync.RWMutex.Lock(),但 Get 方法完全未加锁,这是严重隐患。即使 Get 只读,一旦与 Put 并发执行,就会触发运行时 panic。
✅ 正确做法是:所有对共享 map 的访问(无论读或写)都必须受同步原语保护。推荐使用 sync.RWMutex,它允许多个 goroutine 同时读取,但写操作会独占锁:
// Get 安全读取(加读锁)
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
}
// Put 安全写入(加写锁)
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,
}
}
⚠️ 关键注意事项:
- RLock()/RUnlock() 与 Lock()/Unlock() 不可混用:读锁不能防止其他 goroutine 写入,必须由写锁(Lock)来排他性阻塞所有读写。
- defer 必须紧随锁操作之后,确保异常路径下也能释放锁,否则将导致死锁。
- 不要直接返回 *MemoryCacheValue 的指针(如原代码 return v),应返回副本或只读字段(如 v.value),避免外部 goroutine 修改缓存内部状态。
- 若读操作远多于写操作,RWMutex 比普通 Mutex 更高效;但若写频繁,需评估是否需改用更高级方案(如分片锁、sync.Map 或基于 channel 的 actor 模式)。
? 替代方案参考:sync.Map 适用于读多写少、键值生命周期长的场景,它内部做了优化,但不支持遍历、无 len() 方法,且零值可用无需显式初始化:
type MemoryCache struct {
c sync.Map // 替代 map[string]*MemoryCacheValue
}
func (m *MemoryCache) Get(key string) interface{} {
if v, ok := m.c.Load(key); ok {
return v
}
return nil
}
func (m *MemoryCache) Put(key string, expires int64, value interface{}) {
m.c.Store(key, &MemoryCacheValue{value: value, expires: time.Now().Unix() + expires})
}
但注意:sync.Map 适合简单键值存取,不适合需要原子性组合操作(如“读-改-写”)的场景——此时仍需 RWMutex 或 Mutex。
总结:Go 中没有“只读无需锁”的安全特例。只要共享数据可能被并发修改,所有访问路径(包括纯读)都必须同步。选择 RWMutex 是平衡性能与安全的最佳实践,而 sync.Map 是特定场景下的便捷替代,不可盲目替换。











