go-cache默认配置不高性能,因不限大小、不控锁粒度、set返回值易误判;hand-rolled map+sync.rwmutex易致读慢、goroutine爆炸、锁瓶颈、内存污染及gc失效;初始化须避panic:defaultexpiration禁用0、cleanupinterval必须>0;set返回false仅表key已存在,需首次写入应选add;bigcache分片数需按qps调优,结构体高频变更时性能反低于go-cache。
直接用 go-cache 就能跑通,但“高性能”不等于“默认配置就高性能”——它默认不限大小、不控锁粒度、set 返回值易误判,不调参反而容易拖垮服务。
为什么别一上来就 hand-rolled map + sync.RWMutex
自己封装 map + sync.RWMutex 看似可控,实际踩坑密集:
- 每次
Get()都得检查ExpiredAt字段,读路径变慢; - 定时清理若用
time.AfterFunc为每个 key 单独启 goroutine,10k key 就起 10k goroutine,内存和调度全崩; - 没做分片,高并发写入时
RWMutex成瓶颈,QPS 上不去; - value 存了
map或slice后又原地修改,缓存内容被意外污染; - 忘了在
delete(m, key)后把 value 置nil,GC 无法回收大对象。
go-cache 初始化必须避开的两个 panic 点
cache.New() 传参错一个,启动就 panic,不是运行时报错:
- 第一个参数是
defaultExpiration,不能传0—— 要永不过期,用cache.NoExpiration; - 第二个参数是
cleanupInterval,必须 > 0;传0或负数会触发invalid memory address; - 正确示例:
cache.New(5*time.Minute, 10*time.Second)或cache.New(cache.NoExpiration, 30*time.Second)。
Set() 返回 false 不代表失败,Add() 才是你想要的“首次写入”
这是最常被日志刷屏的误用:
-
cache.Set("k", v, exp):key 存在 → 更新并返回false;key 不存在 → 插入并返回true; - 误写
if !cache.Set(k, v, exp) { log.Println("set failed") }→ 每次更新都打日志; - 需要“只在 key 不存在时才设”,改用
cache.Add("k", v, exp),它失败才返回false,且不覆盖; - 注意:
Add()对已过期但尚未被清理的 key 也视为“存在”,不会写入。
bigcache 分片数(shards)不调,高并发下锁争抢比 map 还狠
bigcache.NewBigCache() 默认 shards = 256,对中小服务够用,但 QPS > 5k 就明显卡顿:
- QPS 在 5k~10k:建议设为
512; - QPS > 10k:设为
1024或更高,但别盲目堆大 —— 每个 shard 固定占几 KB,4096shards 白白吃掉 20MB+ 内存; - value 是结构体且频繁变更?别硬上
bigcache—— 它每次Set()都要序列化 + 拷贝字节,实测可能比go-cache慢 30%; - 必须用
[]byte做 value?优先选SetString(),它自动拷贝底层数组;若用Set(),对外部[]byte显式append([]byte(nil), src)拷贝。
真正难的不是“怎么存”,是“删哪些、什么时候删、删完请求怎么兜住”。所有本地缓存库都不解决缓存击穿——你得自己加 singleflight.Group 或两级 TTL,否则 DB 一瞬间就被打穿。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











