Go本地缓存选型须按写QPS决策:超200 QPS必须用cache.NewSharded(64),读多写少可用cache.New;初始化禁传0或负数,Get后必判found,Set永不过期须显式传cache.NoExpiration。

Go 本地缓存不是“写个 map 加把锁”就能上线的,真要扛住生产流量,必须在选型、初始化、读写、清理四个环节都踩准节奏——错一个参数就 panic,少一次 found 判断就 crash,分片数设小了就卡在锁上。
cache.New 和 cache.NewSharded 怎么选?看写 QPS,不是看教程
cache.New 内部用 sync.RWMutex 全局锁,读不阻塞但所有写串行。实测写入超 200 QPS 就开始排队,CPU 升高、吞吐不涨;cache.NewSharded 把 key 哈希到多个独立子缓存(默认 64 个),每个带自己的锁,写天然并行。
- 读多写少(如地区字典、静态配置)→
cache.New(5*time.Minute, 10*time.Minute) - 写密集(如 session 更新、实时计数器)→ 必须用
cache.NewSharded(64, 5*time.Minute, 10*time.Minute),32–64 分片足够,别盲目设 128(哈希收益趋零,GC 压力反升) - 分片哈希算法是
djb33,不依赖hash/fnv,快且分布均匀,无需自己重写
初始化 panic 的真实原因:参数传错,不是代码 bug
cache.New 初始化失败几乎全是参数违规:传 0 或负数会触发内部未初始化字段访问,直接 panic,不是你代码有逻辑错误。
-
defaultExpiration不能传0—— 正确写法是cache.NoExpiration;传0会被转成time.Duration(0),后续除零或 nil 解引用崩溃 -
cleanupInterval必须 > 0;传0或负数会跳过后台 goroutine 启动,但内部仍尝试读 timer 字段,panic - 常见翻车写法:
cache.New(0, 0)、cache.New(time.Second, 0)、cache.New(-1, time.Minute) - 正确示例:
cache.New(cache.NoExpiration, 30*time.Second)或cache.New(10*time.Minute, 2*time.Minute)
Get 后必须检查 found,否则 nil 解引用或判断失效
cache.Get 返回 (interface{}, bool),Go 不做隐式转换。漏掉 bool 判断会导致两种静默问题:key 不存在时返回 nil,强制 .(*User) 直接 panic;用 == nil 判断则因 interface{} 的 nil 和指针 nil 不等价而失效。
立即学习“go语言免费学习笔记(深入)”;
- 错误写法:
user := c.Get("user:123").(*User)或if user := c.Get("user:123"); user != nil { } - 正确写法:
if v, found := c.Get("user:123"); found { user := v.(*User) } - 结构体建议存指针:
c.Set("user:123", &u, ttl),避免值拷贝开销
Set 传 0 或不传 duration 会怎样?永不过期 ≠ 传 0
cache.NoExpiration 是常量,不是默认值;Set 时不传 duration 会 panic,传 0 ≠ 永不过期,而是“立即过期”。
- 想永不过期 → 必须显式传
cache.NoExpiration - 想设 30 秒 → 显式写
c.Set("key", val, 30*time.Second),别依赖构造时的 “默认” -
cache.NoExpiration不代表真正永存:它跳过后台扫描,但仍受 GC 和c.Flush()影响;Items() 返回的是 map 快照,不是实时状态,不能用来判断 key 是否“一定存在” - 业务类长期缓存建议设非零 TTL,比如
24 * time.Hour,避免被误判为垃圾
最易被忽略的点是:后台清理是惰性的,Get 时不会主动检查过期,只靠定时扫描;所以高频写场景下,得靠 Get 时手动判断 expireAt,或者干脆用 cache.NewSharded 配合合理 cleanupInterval 控制延迟上限。


















