go框架不内置缓存淘汰策略,需依赖第三方库或手写结构;context.set仅限单请求生命周期,无法实现跨请求缓存与淘汰;ristretto、lru.cache、lfu及sync.map各有淘汰机制缺陷,真正难点在于淘汰逻辑的性能与正确性平衡。

Go 框架本身(如 net/http、gin、echo)不内置缓存淘汰策略——它们只提供中间件钩子或响应缓存控制头,真正的淘汰逻辑必须由你选的缓存库或手写结构承担。直接用框架自带 map 或 sync.Map 做缓存,等于裸奔。
为什么 gin/echo 的 context.Set 不适合做带淘汰的缓存
框架的 context.Set / c.Set 只是往请求上下文里塞值,生命周期仅限单次 HTTP 请求;它不跨请求共享,更不支持容量限制、频次统计、过期检查或后台驱逐。试图在中间件里用它模拟“全局缓存”,结果必然是内存泄漏 + 无淘汰 + 并发错乱。
- 它底层是
map[interface{}]interface{},没有键顺序、无法遍历、不能配合 list 做 LRU - 没有时间字段,没法做 TTL 判断;没有计数器,没法做 LFU
- 每次请求新建 context,旧值自动丢弃——你以为在复用,其实只是没清理干净的幻觉
真正落地时,你实际依赖的是这三个层级之一
你在框架里写的“缓存逻辑”,最终都会落到下面某一层,而每层的淘汰行为完全不同:
-
ristretto:调用
cache.Set(key, value, cost)时传ristretto.TTL,淘汰由后台 goroutine 异步触发,基于采样 + cost 估算,不依赖访问时间戳;Get是无锁读,但淘汰回调里拿不到原始 key 字符串(只有 hash),不适合文件路径类缓存 -
lru.Cache + 懒检查:用
github.com/hashicorp/golang-lru/v2,Set存struct{ value interface{}; expireAt time.Time },Get先取再判断expireAt.Before(time.Now()),过期就Delete;淘汰只按链表长度,不看时间,所以必须靠 Get 时主动清理 -
手写 LFU(含时间 fallback):必须同时维护
freqMap map[int]*list.List、keyNodeMap map[string]*node和minFreq int;Get后要把节点从旧频次链表Remove,再PushFront到新频次链表;频次相同时,链表尾部才是最久未用——漏掉Remove再插入这一步,节点会卡在错误链表里
sync.Map 在框架中间件里最容易被误用
很多人在 gin 中间件里写 var cache = sync.Map{},然后 cache.Store(key, value),以为这就叫“高性能缓存”。但问题立刻暴露:
-
sync.Map.Range无序且非原子,你没法实现“淘汰最久未用”或“找出所有过期项” - 没有访问顺序记录,
Range返回的 key 可能刚被访问过,也可能三天没碰过 - 想加过期?得自己起 goroutine 定时遍历——但
Range不保证遍历期间数据不变,删着删着 panic - 实测压测 2 小时后,缓存项数量翻 5 倍,RSS 内存持续上涨,最后 OOM killer 杀进程
真正难的不是写个 Put 和 Get,而是让淘汰不拖慢主流程、不引发 GC 尖峰、不因系统时间跳变误删、不因并发 Get 同时删 key 导致日志刷屏。这些全藏在 time.Now() 调用位置、map 删除时机、以及是否多分配了一个 time.Time 字段里。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











