goburrow/cache(或go-cache)比sync.Map更适合业务缓存,因其支持自动过期清理、TTL写入、驱逐回调等缓存核心能力,而sync.Map仅为线程安全KV映射,无过期与淘汰机制。

为什么 goburrow/cache 比原生 sync.Map 更适合业务缓存
因为 sync.Map 只提供基础线程安全的 KV 存取,没过期、没淘汰、没统计——它不是缓存库,是并发映射工具。而 goburrow/cache(或更常用的 github.com/patrickmn/go-cache)才真正按缓存语义设计:自动清理过期项、支持基于 TTL 的写入、能回调驱逐事件。
常见错误现象:sync.Map 里存了大量过期数据却无法感知,内存只增不减;或者自己手写定时器清理,结果和写操作竞争导致 panic。
- 使用场景:HTTP 请求级临时 token 缓存、配置项热加载兜底、数据库查询结果短时复用
-
go-cache默认无持久化、纯内存、非分布式——适合单进程内轻量级缓存 - 性能影响:读写都是 O(1),但开启
DefaultExpiration后每次写入会启动 goroutine 延迟清理,高并发写入时注意 GC 压力
初始化时必须设对 cleanupInterval,否则过期项永不删除
go-cache 不在写入时实时检查过期,而是靠后台 goroutine 定期扫描。如果初始化时把 cleanupInterval 设为 0 或负数,清理协程根本不会启动,缓存只会膨胀,直到进程 OOM。
正确做法:
立即学习“go语言免费学习笔记(深入)”;
cache := cache.New(5*time.Minute, 10*time.Minute)
这行代码中第二个参数就是 cleanupInterval——它和过期时间无关,只是“每隔多久扫一次”。典型值是 30s 到 5m,太短增加调度开销,太长导致过期项滞留久。
- 别用
cache.New(0, 0)试图“禁用过期”,这等于关掉清理器 + 所有条目永不过期 - 如果某条目需精确控制过期,用
Set(key, value, cache.DefaultExpiration)覆盖全局默认值 - 兼容性注意:v2+ 版本已弃用
New(),改用cache.NewCache(),参数顺序也变了
Get 返回 (interface{}, bool),漏判 bool 是最常踩的坑
很多开发者只接第一个返回值,误以为 nil 就代表不存在,结果遇到存了 nil 值的 key(比如接口返回空结构体指针)就逻辑错乱。
正确写法永远要检查第二个布尔值:
if val, found := cache.Get("user:123"); found {<br> // 处理 val<br>} else {<br> // 真正未命中,该查 DB 或生成新值<br>}
- 即使你确定不会存
nil,也得判found——因为过期、被驱逐、从未写入,都走这个分支 - 不要用
val == nil判断是否命中,Go 中interface{}的 nil 和底层具体类型的 nil 不等价 - 如果想统一处理未命中逻辑,封装一层
GetOrSet(key string, fetch func() interface{}) interface{}
缓存值类型必须可序列化,否则 Set 后读出来是零值
go-cache 内部不做深拷贝,直接存引用。但如果值是含不可导出字段、func 类型、map/slice 指向共享底层数组的结构体,后续修改原变量会导致缓存内容意外变更。
更隐蔽的问题:某些 ORM 模型对象带 sync.RWMutex 字段,Get 出来后调 Lock() 会 panic——因为 mutex 已失效。
- 推荐只缓存简单类型:
string、int、struct{}(且所有字段导出)、[]byte - 避免缓存指针类型,除非你能确保生命周期可控;如必须用,
Set前做浅拷贝 - 如果值较大(>1MB),考虑用
unsafe.Sizeof预估,避免单 key 占满内存
实际项目里,最麻烦的从来不是怎么加缓存,而是缓存失效时机和一致性边界没想清楚。比如 DB 更新后要不要删 cache、并发写入时要不要加锁、不同服务实例间要不要同步——这些 go-cache 一概不管,它只负责把东西暂时放好。










