sync.Map仅适用于读≥95%、键稳定、无TTL/遍历需求的极窄场景;误用会导致写卡顿、内存只增不减、删不干净;真需本地缓存应选bigcache或go-cache。

Go 本地缓存不是选“用不用”的问题,而是选“用哪个、怎么控、怎么防崩”的问题。直接上 map + sync.RWMutex 能跑,但一上生产就容易内存涨、GC 崩、并发错;用 sync.Map 看似省事,实则写多就卡顿、删不干净、永远不过期;而 go-cache 初始化错一个参数就 panic,bigcache 分片设小了锁争抢严重——这些坑都踩过才敢说:缓存的难点从来不在“存”,而在“删得准、控得住、扛得稳”。
go-cache 初始化 panic 的真实原因和修复方式
go-cache 初始化时 panic(比如 "invalid memory address" 或 "cleanupInterval must be greater than 0")几乎全是参数传错导致的,不是代码 bug。
-
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)
sync.Map 为什么不能当主力缓存用
sync.Map 不是 map 的线程安全平替,它是为极特定场景设计的读多写少结构,强行塞进缓存逻辑里反而埋雷。
- 写操作(
Store)会把整个dirty map复制一遍,key 超过几百个、每秒写几十次,内存增长飞快,GC 频繁 - 删除(
Delete)只是标记,不会从dirty map中物理移除,已删 key 仍占内存,长期运行后 RSS 持续上涨 - 完全不支持 TTL、LRU、大小限制 —— 你得自己包一层定时器+计数器,代码量和出错概率远超直接用
gcache - 适合场景仅限:几十个固定配置项(如
configMap["timeout"])、连接池元数据(connPool["db-01"]),且写入后基本不更新
bigcache 分片(shards)怎么设才不翻车
bigcache 的性能高度依赖 shards 参数,设错会导致锁争抢或内存浪费,没有“默认值能扛住一切”的说法。
立即学习“go语言免费学习笔记(深入)”;
- 默认
shards = 256,对 QPS pprof 里能看到明显runtime.futex占比飙升 - QPS > 10k 时建议 ≥ 512;再往上可按
shards = 1024试,但别盲目堆大 —— 每个 shard 至少占几 KB 固定开销,4096 shards 就白吃 16MB+ 内存 - value 是结构体且频繁变更时,
bigcache反而比go-cache慢:每次Set都要序列化 + 拷贝字节,而go-cache存的是指针引用 - 它不支持原生 TTL,必须自己在 value 结构里加
expireAt int64字段,并在Get时手动检查,否则过期数据永远滞留
自己手写 LRU 缓存最容易漏掉的三个原子性细节
用 container/list + map 实现 LRU 看似简单,但 Get/Put/Delete 三步操作若不同步,缓存会快速腐烂:map 有 key、list 没节点,或反之。
- Get 之后必须
MoveToFront,且这个动作和 map 查找必须在同一个mu.Lock()里完成,否则并发下可能刚 Move 完就被另一个 Put 清掉尾部 - Put 满容时淘汰尾节点,必须先从
mapdeletekey,再调用list.Remove();顺序反了会导致节点还在链表里但 map 找不到,GC 不回收,内存泄漏 - 不要复用
*list.Element或自定义node结构体实例 —— Go 没有对象池自动管理,复用极易引发字段残留、并发读写冲突;每次新建更安全 - 如果需要 TTL,别在节点里硬塞时间戳然后全量扫描清理 —— 正确做法是惰性检查:
Get时发现过期就delete并返回 miss,避免写路径变慢
真正难处理的从来不是“怎么存”,而是“删哪些、什么时候删、删完 cache 是否还一致”。所有看似简单的缓存库,都在用不同方式绕开这三个问题;选型前先问清楚:你的 key 数量级、读写比、value 大小、是否需要过期、能否接受偶尔 miss —— 答案不同,方案就完全不同。


















