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 定期扫描清理——小规模缓存没必要,且容易引发误删(比如刚写入就扫到了);按需惰性清理更简单可靠。
立即学习“go语言免费学习笔记(深入)”;
1、数据调用该功能使界面与程序分离实施变得更加容易,美工无需任何编程基础即可完成数据调用操作。2、交互设计该功能可以方便的为栏目提供个性化性息功能及交互功能,为产品栏目添加产品颜色尺寸等属性或简单的留言和订单功能无需另外开发模块。3、静态生成触发式静态生成。4、友好URL设置网页路径变得更加友好5、多语言设计1)UTF8国际编码; 2)理论上可以承担一个任意多语言的网站版本。6、缓存机制减轻服务器
- 存储时用
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,中间封装掩盖了真实行为,调试时更难定位问题。
- 如果只是存 token、配置、用户基本信息,手写 50 行以内的缓存足够用
- 若后续出现内存持续上涨,优先检查是否忘了设 TTL,而不是立刻换库
- 真正需要替换的信号是:缓存命中率长期低于 60%,且 key 分布明显倾斜(少数 key 占 80% 流量)
测试缓存并发安全性的关键点
光跑单测不等于线程安全。很多 bug 只在高并发下暴露,比如 LoadOrStore 和 Delete 交错导致短暂脏读,或过期判断和删除之间被其他 goroutine 插入旧值。
用 go test -race 是底线,但还不够。建议写一个压力测试函数,混合执行 Get/Set/Delete,并断言命中率、无 panic、无重复初始化。
- 并发写同一 key 时,
LoadOrStore应保证只初始化一次(适合初始化 DB 连接池等) - 并发读+过期删除时,确保不会返回已删除的值,也不会 panic(比如类型断言失败)
- 避免在
Get中直接返回指针或可变结构体——外部修改会影响缓存内容,应考虑深拷贝或只缓存不可变类型









