Go本地缓存关键在于分层选型与避坑:map+sync.RWMutex需手动处理TTL和GC;go-cache的Set()返回值表示是否首次插入而非操作成败;bigcache须合理设置shards参数;metrics应聚合统计而非高频埋点;key设计不当会直接导致缓存失效。

Go 本地缓存不是“要不要加”的问题,而是“加在哪一层、怎么避免踩坑”的问题。直接用 map + sync.RWMutex 能跑通,但并发高、key 多、value 变更频繁时,大概率会遇到锁争抢、内存泄漏、TTL 失效或误判 Set 返回值等问题。
用 go-cache 时为什么 Set() 返回 false 却没报错?
这是正常行为,不是 bug。go-cache 的 Set() 返回 bool 表示“是否为首次插入该 key”,而非“操作是否成功”。
- key 已存在 → 更新 value 并返回
false - key 不存在 → 插入并返回
true - 内部 map 扩容失败(极罕见)→ 不抛 error,目前代码也未暴露错误路径
常见误用:if !cache.Set(k, v, exp) { log.Println("set failed") },结果日志刷屏。真正需要“首次写入”语义时,应改用 Add():它只在 key 不存在时写入,失败才返回 false,且不覆盖。
用 bigcache 前必须调大 shards 参数
bigcache 靠分片(shard)降低锁粒度,但初始化时传的 shards 默认是 256,对中高并发场景偏小。
立即学习“go语言免费学习笔记(深入)”;
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- QPS
- QPS > 10k:建议 ≥ 512,否则多个 goroutine 会频繁竞争同一 shard 锁
- shards 过大(如 > 4096):浪费内存,且每个 shard 至少占几 KB 固定开销
- value 频繁变更时,
bigcache比go-cache慢——因为每次Set()都要序列化 + 拷贝字节,而go-cache存的是指针
如果你的 value 是结构体且常更新,别盲目追求 bigcache 的“高性能”标签,先压测对比。
自己手写 map + sync.RWMutex 缓存要注意 TTL
原生 map 不带过期逻辑,硬加时间戳字段 + 每次 Get() 检查,会拖慢读性能;全量扫描清理又影响写。
- 简单场景(少量 key、低频访问):在
Get()里检查expireAt字段,过期就delete()并返回 miss - 中等规模(百级 key、每秒几十次读):启动一个后台 goroutine 定期清理,比如每 30 秒扫一次,用
time.AfterFunc链式触发 - 别在
Set()里同步删过期 key——这会让写变慢,且无法解决“长期不读导致过期 key 堆积”的问题
另外,map 本身不支持 GC 友好回收:如果 value 是大对象(如 []byte、struct{} 嵌套多层),即使 key 被删,只要引用还挂在 map 里,GC 就不会回收。记得 delete(m, key) 后显式置空引用(如 m[key] = nil)。
本地缓存要不要加 metrics?加在哪最有效?
要加,但别埋在每行 Get()/Set() 里——高频调用下打点本身就成了性能瓶颈。
- 推荐在缓存 wrapper 层统一统计:命中数、未命中数、平均耗时(用
time.Since()包裹核心逻辑) - 避免用
atomic.AddUint64频繁累加计数器,改用周期性 flush 到 Prometheus 或日志(比如每 10 秒聚合一次) - 最关键的指标其实是“未命中后回源耗时”:它能暴露缓存策略是否合理,比如 DB 查询慢了,光加缓存没用
真正容易被忽略的是:缓存 key 设计是否可缓存。比如用完整 HTTP 请求 URL 当 key,但其中含 timestamp 或随机参数,会导致缓存完全失效——这种问题比选哪个库更致命。

















