Go 的 map 本身不并发安全,多 goroutine 读写必 panic;必须用 sync.RWMutex 封装并确保所有访问(含 len、range)均加锁,且避免值逃逸;简易缓存无需 TTL,复杂需求应选成熟库;sync.Map 仅适用于读多写少、无需遍历的简单场景。

为什么不能直接用 map 当缓存
Go 的 map 本身不是并发安全的,一旦多个 goroutine 同时读写,程序会 panic 并输出 fatal error: concurrent map read and map write。这不是概率问题,是必现的运行时错误。哪怕只有 1 个写 + 多个读,也必须加锁——sync.RWMutex 就是为此设计的:读多写少场景下,允许多个读同时进行,只在写时阻塞全部读写。
sync.RWMutex 怎么和 map 组合才不出错
封装时最容易漏掉的是:所有对 map 的访问(包括 len()、range 遍历、类型断言)都必须包裹在 RWMutex 的锁区内。常见错误包括:
- 读操作用了
RWMutex.RLock(),但忘了调用defer mu.RUnlock() - 写操作用了
mu.Lock(),但在make新map或delete后提前 return,导致锁没释放 - 把
map值作为结构体字段直接返回,外部修改该值间接污染缓存(比如存了[]byte或指针)
正确做法是把 map 和 RWMutex 封进一个 struct,并只暴露方法接口:
type Cache struct {
mu sync.RWMutex
data map[string]interface{}
}
func (c *Cache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.data[key]
return v, ok
}
func (c *Cache) Set(key string, value interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
if c.data == nil {
c.data = make(map[string]interface{})
}
c.data[key] = value
}
要不要加过期时间?不加比乱加更安全
简易缓存的核心目标是“避免并发崩溃 + 快速读写”,加 TTL(time-to-live)会立刻引入三个复杂点:
立即学习“go语言免费学习笔记(深入)”;
- 需要后台 goroutine 定期扫描清理,带来资源开销和生命周期管理负担
- 读操作可能要检查过期时间,得从
RWMutex.RLock()升级为Lock()(因为要删过期项),读性能下降 - 时间判断依赖
time.Now(),在容器或虚拟机中可能因系统时间跳变导致批量误删
如果真需要过期,建议用现成库如 github.com/patrickmn/go-cache,而不是自己拼 time.AfterFunc 或 map[time.Time]。自己实现的“简易”缓存,就只做无过期的键值存取。
什么时候该换用 sync.Map
sync.Map 是 Go 标准库提供的并发安全 map,但它有明确适用边界:
- 适合读远多于写的场景(比如配置表、白名单),且 key 类型固定为
string或int等基础类型 - 不支持遍历全量数据(
Range()是唯一方式,无法控制顺序或中途退出) - 内存占用比普通
map+RWMutex高,因为内部用了分片 + 延迟初始化策略 - 没有原子的“读-改-写”操作(比如 CAS),需要自己用
Load/Store配合循环重试
结论:如果你的缓存只是临时存放少量中间结果、更新不频繁、又不想自己管锁,sync.Map 可以省事;但一旦需要遍历、清空、或嵌套结构体值,还是老老实实用 map + RWMutex 更可控。
真正容易被忽略的点是:缓存对象的生命周期和所属 goroutine 的关系。比如把一个 *Cache 传给多个 goroutine 没问题,但若在某个 goroutine 里把 Cache.data 直接赋值给全局变量或 channel,就等于绕过了锁保护——这种“逃逸”不会报错,但会悄悄破坏一致性。










