Go中真正可用的LRU缓存关键在于并发安全、淘汰回调与容量边界的正确处理:需封装cacheEntry结构体携带key/value,用sync.RWMutex保护list操作,淘汰前收集项并解锁后执行回调,且按内存占用而非元素个数控制容量。

Go 里实现一个真正可用的 LRU 缓存,关键不是写对 MoveToFront,而是处理好并发、淘汰回调和容量边界这三件事。标准库 container/list + map 组合能跑通逻辑,但直接用于生产环境大概率出问题。
为什么不能只用 list.List 和 map 就完事?
常见错误是把 list.Element 当作值存进 map,然后在 Get 时调用 MoveToFront —— 这本身没错,但漏掉了两个致命细节:
-
list.Element不自带 key 或 value 信息,必须额外包装成结构体(比如cacheEntry),否则淘汰时根本不知道删的是谁 -
list.List非线程安全,高并发下MoveToFront和Remove可能同时操作同一节点,导致 panic 或数据错乱 - 没处理缓存满时的“先删后插”顺序:如果先插新节点再删尾部,实际容量会临时超限
sync.RWMutex 用法容易被低估
读多写少场景下,用 sync.RWMutex 而非 sync.Mutex 是基本要求,但很多人只注意了锁粒度,忽略了锁释放时机:
-
Get方法必须用RUnlock配对RLock,且要在访问list.Element.Value之后才移动节点 —— 否则MoveToFront期间其他 goroutine 可能已修改该节点 -
Put中的淘汰逻辑(RemoveOldest)必须在写锁内完成,否则可能删掉刚被Get刷新过的节点 - 回调函数
OnEvicted不能在锁内执行,否则会阻塞整个缓存;应先收集待淘汰项,解锁后再调用
淘汰时怎么拿到 key 和 value?
这是最常被跳过的一步。list.Element.Value 是 interface{},必须做类型断言才能取值:
立即学习“go语言免费学习笔记(深入)”;
- 定义统一的 entry 结构体:
type cacheEntry struct { key, value interface{} } - 插入时:
element := c.usageList.PushFront(&cacheEntry{key: key, value: value}) - 淘汰时:
entry := element.Value.(*cacheEntry),再传给回调或日志 - 如果 key 类型不确定,避免用
string做 map key,用interface{}更通用,但需确保 key 实现了Equal或可比较
容量控制别只看元素个数
很多实现只统计 len(cache),但真实业务中 value 可能很大(比如缓存图片 bytes):
- 需要让 value 实现
Len() int接口,动态计算内存占用 -
Nbytes字段要原子更新:atomic.AddInt64(&c.Nbytes, int64(newSize-oldSize)) - 淘汰循环必须用
for c.Nbytes > c.maxBytes而非if,因为单次淘汰可能不够
真正难的不是链表怎么连,而是当缓存命中率下降、GC 压力上升、回调函数 panic 时,你能不能快速定位到是锁顺序错了,还是 Value 类型断言失败了。


















