直接套用container/list实现LRU易出错,因list.Element被移除后map仍持有其指针会导致MoveToFront panic;且map与list并发读写不加锁会引发数据竞争;正确做法是所有链表操作与map更新必须共用同一锁,删除时先list.Remove(e)再delete(map,key),Get/Put需用sync.RWMutex或sync.Mutex保护。

Go 里用 container/list 实现 LRU 为什么容易出错
直接套用 container/list 写 LRU,最常踩的坑是没处理好指针生命周期和并发安全。比如 list.Element 被移除后还被 map[string]*list.Element 持有,后续调用 MoveToFront 会 panic;又或者多个 goroutine 同时读写 map 和 list,没加锁导致数据错乱。
实际做法是:所有对 list 的操作(MoveToFront、Remove、PushFront)必须和 map 更新放在同一临界区;list.Element 不应暴露给外部,只在结构体内维护;Get 和 Put 都要加 sync.RWMutex 或 sync.Mutex,读多写少时用前者更合适。
-
map中存的是*list.Element,不是值,所以删除前必须先list.Remove(e)再delete(map, key) -
Put里更新已有 key 时,不能只改e.Value,还要调用dl.MoveToFront(e),否则访问顺序不会刷新 - 如果缓存项实现了
Len() int接口(如自定义 struct),Put时要重新计算内存占用,避免Nbytes累加错误
LFU 在 Go 中必须额外维护频率计数器
LRU 只需维护时间顺序,LFU 必须按访问频次分组管理——这是核心差异。不能只用一个 map[string]int 记频次再排序,那样每次 Get 都要遍历全量 map 找最小频次项,O(n) 太慢。
高效做法是:用一个 map[int]*list.List 存储每个频次对应的所有 key(即“频次桶”),再用一个 map[string]*freqNode 快速定位某个 key 所在的桶和节点;每次 Get,把 key 从当前桶移到 freq+1 桶,并更新指针;淘汰时,从最低非空桶的尾部删节点。
立即学习“go语言免费学习笔记(深入)”;
- 频次为 1 的新 key 插入时,要检查
freqMap[1]是否存在,不存在就新建list.List并插入 - 当某个桶变空(
list.Len() == 0),必须从freqMap中delete,否则最低频次判断失效 -
Put时若触发淘汰,必须从最低频次桶中选一个节点删,而不是任意删——否则违背 LFU 语义
LRU 和 LFU 的性能拐点在哪
LRU 的单次 Get 是 O(1),LFU 的单次 Get 也是 O(1),但 LFU 的常数更大:一次 Get 至少涉及两次 map 查找(key→node、freq→list)、一次 list 移动、可能还有桶创建或删除。当缓存规模小(
真实场景中,LRU 更适合高频更新、访问时间集中的数据(如用户 session、实时行情);LFU 更适合低频但强热点的数据(如静态资源路径、配置项)。别硬套 LFU 试图“优化一切”,它对冷热切换敏感——一个曾经高频的 key 突然停用,它的频次不会自动衰减,得靠额外机制(如带 TTL 的频次滑动窗口)补救。
- Go 标准库没有内置 LFU,
container/list也不支持按频次自动分桶,必须手写两层 map + list 结构 - 如果业务允许容忍少量过期热点,LRU 实现更轻量、更易 debug;LFU 一旦逻辑出错(比如频次没更新或桶没清理),缓存污染会持续很久
- 用
pprof对比两者在真实请求压测下的 CPU 占比和 GC 压力,比理论分析更可靠
如何让 LRU/LFU 支持带内存限制的自动驱逐
单纯按数量限制(capacity int)太粗糙。Go 里更实用的是按字节限制(maxBytes int64),尤其缓存 value 是 []byte、struct 或嵌套 map 时。关键在于:value 必须实现 Len() int 接口,且这个方法返回的必须是真实内存占用(不是 len() 或 unsafe.Sizeof())。
例如缓存一个 JSON 字符串,Len() 应返回 len([]byte);缓存一个 struct,得手动累加各字段大小(考虑指针、slice header 等)。驱逐逻辑不是“删一个就停”,而是循环调用 RemoveOldest 直到 Nbytes 。
-
Add方法里,更新已有 key 时要先减去旧 value 的Len(),再加新 value 的Len(),否则内存统计失真 - 驱逐触发的回调
OnEvicted函数里,别做耗时操作(如写日志、发 HTTP 请求),否则阻塞整个缓存写入流程 - 如果 value 是大对象(>1MB),建议在
Put前做 shallow copy 或引用计数,避免意外共享导致内存泄漏



















