直接用sync.mutex+container/list.list容易出错,因其非并发安全,查删插操作若未严格原子执行会导致panic、key丢失或命中率异常;必须共用同一锁保护map与list操作,且所有element访问前需确认其仍属当前链表。

为什么直接用 sync.Mutex + list.List 容易出错
Go 标准库的 list.List 本身不是并发安全的,哪怕你在外层加了 sync.Mutex,也容易在「查、删、插」三步操作中漏掉边界情况。比如:查到节点后解锁再操作,中间被其他 goroutine 修改了链表;或者忘记把节点从原位置移除就直接 MoveToFront,导致重复插入或 panic。
典型错误现象:panic: runtime error: invalid memory address or nil pointer dereference(访问已移除节点的 Next())、缓存命中率异常低、偶发 key 丢失。
- 所有对
list.Element的访问前,必须确认它仍属于当前*list.List -
list.List.Remove后,该*list.Element就失效了,不能再调用Value或Next - 读写共享 map 和 list 必须共用同一把锁,不能读用 RWMutex、写用 Mutex 混用
如何正确封装 Get 和 Put 方法
核心逻辑是「查得着就挪到头,查不到就新建并检查容量」,但每一步都要在锁内完成,且避免重复分配。
关键点在于:不要在锁外构造新节点,也不要提前释放锁;map 查找和 list 操作必须原子执行。
-
Get中查到 key 后,立即l.list.MoveToFront(elem),然后return elem.Value—— 不要先Remove再PushFront -
Put时若 key 已存在,只更新 value 并MoveToFront;若不存在,先PushFront新节点,再更新 map,最后检查长度是否超限 - 淘汰逻辑放在
Put尾部:当len(l.cache) > l.capacity时,取l.list.Back(),从 map 删除对应 key,再l.list.Remove
list.Element 的 value 类型怎么设才不踩坑
不能直接存原始值(如 string 或 int),因为 list.Element.Value 是 interface{},类型断言易出错;也不能存指针到栈变量(goroutine 返回后悬空)。
推荐统一用结构体指针,把 key 和 value 都包进去,既避免类型转换,又确保生命周期可控。
type entry struct {
key string
value interface{}
}
// 存入:l.list.PushFront(&entry{key: k, value: v})
// 取出:e := elem.Value.(*entry); return e.value
- 不要用
map[string]interface{}当 value,会多一层间接寻址且难 debug - 如果 value 是大对象(如
[]byte),结构体里存指针更省内存,但需确保调用方不复用底层数组 - 避免在
entry里存闭包或带 goroutine 的对象,容易引发泄漏
容量变更与并发重置的隐藏风险
LRU 实例一旦开始服务,capacity 通常不应动态改 —— 改了不会自动触发清理,旧数据可能长期滞留,导致实际内存占用远超预期。
如果真要支持 resize,必须加额外同步机制:要么阻塞所有读写做全量扫描裁剪,要么引入版本号 + lazy cleanup,复杂度陡增。
- 初始化时就定死
capacity,文档明确标注「不可变」 - 测试时重点覆盖「刚好满容 + Put 新 key」和「连续 Get 已存在 key」两种路径
- 线上若发现内存持续上涨,优先查是否误将大对象(如未 Close 的
*os.File)塞进了 cache
真正麻烦的从来不是锁怎么加,而是节点生命周期和 map-key 一致性——只要有一处没对齐,就会出现「查得到却拿不到值」或者「删不掉的幽灵节点」。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











