go-cache本质是带TTL的线程安全map,常见错误包括传0/负数导致panic、误读Get返回值、忽略Close致goroutine泄漏;正确用法:用cache.NoExpiration代替0、cleanupInterval必须>0、以found判断命中、复用实例并调用Close。

go-cache 不是“开箱即用的缓存池”,它本质是一个带 TTL 的线程安全 map[string]interface{},初始化错参数、误读 Get() 返回值、忽略清理 goroutine 生命周期,三分钟就能让服务内存持续上涨直至 OOM。
cache.New() 一运行就 panic,怎么 fix
常见错误是把 0 或负数传给 cache.New() 的两个参数:defaultExpiration 和 cleanupInterval。Go-cache 内部会将 0 转为 time.Duration 后做指针解引用,直接 panic。
-
defaultExpiration设为永不过期,必须用cache.NoExpiration,不是0 -
cleanupInterval必须 > 0;设为0不仅不清理,还会 panic - 别写
cache.New(0, 0)—— 这等于关掉清理器 + 所有条目永不过期,内存只增不减 - 推荐组合:
cache.New(5*time.Minute, 1*time.Minute)或cache.New(cache.NoExpiration, 30*time.Second)
Get() 返回 (nil, false) 却刚 Set 过,怎么回事
这不是并发 bug,而是语义误读:Get() 返回 (interface{}, bool),第二个 bool 值才是判断依据,nil 本身不能说明未命中。
- 存了
nil *User,Get()返回(nil, true),不是未命中 - 存了
map/slice/chan等引用类型,后续修改原变量会“意外”改掉缓存内容 - 并发写入时传指针,多个 goroutine 共享底层数据,
Get()看似一致,实为同一块内存 - 正确写法永远是:
if val, found := cache.Get("key"); found { /* 处理 val */ } else { /* 真未命中,该查 DB */ }
Set() 返回 false 就代表失败?
完全不是。cache.Set() 返回 bool 表示“是否首次插入该 key”,和操作成败无关。它几乎不会失败(内部 map 扩容失败极罕见,且不暴露 error)。
立即学习“go语言免费学习笔记(深入)”;
- key 已存在 → 更新 value 并返回
false - key 不存在 → 插入并返回
true - 误用
if !cache.Set(k, v, exp) { log.Println("set failed") }会导致日志刷屏 - 需要“首次设置”语义,请用
cache.Add(key, value, exp),它只在 key 不存在时写入,失败才返回false - 想实现“有则跳过、无则设且带过期”,只能自己先
Get()判断,go-cache 不提供 CAS 类型操作
为什么缓存用了还是扛不住突发流量
因为 go-cache 没有容量限制、没有淘汰策略(如 LRU/LFU)、不控制总 key 数量。它只靠 TTL + 定时扫描清理,内存只增不减是常态。
- 高写入场景下,key 数量无上限,极易触发 OOM
- 混合使用永不过期(
cache.NoExpiration)和有时效的 key,清理 goroutine 仍要遍历全表 - 反复创建新 cache 实例(比如 HTTP handler 里每次请求都
new cache),旧实例的清理 goroutine 不会自动退出,会堆积泄漏 - 生产环境务必复用 cache 实例(全局或按业务域单例),必须动态生命周期时,销毁前显式调用
cache.Close()
最常被忽略的点:cache.Close() 不是可选操作,它是防止 goroutine 泄漏和内存滞留的必要步骤;而 found 这个布尔值,比 val == nil 更值得你多敲一次键盘去判断。


















