sync.Map 是高并发读多写少场景下实现线程安全缓存的轻量首选,支持 LoadOrStore 等原子操作,但需手动添加 TTL 和惰性过期清理,小规模场景无需第三方库。
sync.Map 实现线程安全的简单缓存Go 标准库没有开箱即用的“缓存”类型,但 sync.Map 是最轻量、最常用的选择——它专为高并发读多写少场景设计,避免了全局锁带来的性能瓶颈。
直接用 map 加 sync.RWMutex 也能做,但要自己处理键存在性判断、删除逻辑、零值覆盖等问题;sync.Map 的 LoadOrStore 和 CompareAndDelete 等方法天然规避了竞态风险。
Load 返回 (value, ok),和普通 map 一致,适合查缓存Store 总是覆盖,LoadOrStore 仅在键不存在时才写入,适合初始化默认值time.Now() 判断标准 sync.Map 不带 TTL(Time-To-Live),必须手动处理。常见做法是在 value 中嵌入过期时间戳,每次 Load 后检查是否过期,过期则 Delete 并返回未命中。
不要用后台 goroutine 定期扫描清理——小规模缓存没必要,且容易引发误删(比如刚写入就扫到了);按需惰性清理更简单可靠。
time.Now().Add(ttl) 计算过期时间,和 value 一起存进 sync.Map
Load,再比对当前时间与存储的过期时间,time.Now().After(expireTime)
Delete,避免下次再查一遍重复判断type CacheItem struct {
Value interface{}
ExpireAt time.Time
}
func (c *Cache) Get(key string) (interface{}, bool) {
if item, ok := c.m.Load(key); ok {
if ci, ok := item.(CacheItem); ok {
if time.Now().Before(ci.ExpireAt) 
{
return ci.Value, true
}
c.m.Delete(key)
}
}
return nil, false
}
gocache 或 ristretto
项目刚起步、QPS 不高、缓存条目几百以内时,引入这些库反而增加复杂度:要理解其驱逐策略(LRU/LFU)、配置参数(shards、buffer size)、生命周期管理(Stop/Reset),还可能带来非预期的内存占用。
ristretto 虽然高性能,但它的核心价值在百万级 QPS + GB 级缓存场景;而 gocache 抽象层多,底层仍依赖 sync.Map 或 map,中间封装掩盖了真实行为,调试时更难定位问题。
光跑单测不等于线程安全。很多 bug 只在高并发下暴露,比如 LoadOrStore 和 Delete 交错导致短暂脏读,或过期判断和删除之间被其他 goroutine 插入旧值。
用 go test -race 是底线,但还不够。建议写一个压力测试函数,混合执行 Get/Set/Delete,并断言命中率、无 panic、无重复初始化。
LoadOrStore 应保证只初始化一次(适合初始化 DB 连接池等)Get 中直接返回指针或可变结构体——外部修改会影响缓存内容,应考虑深拷贝或只缓存不可变类型