不能只用map实现LRU,因其无访问顺序,无法O(1)移动节点或淘汰最久未用项;必须结合container/list双向链表(提供O(1)移动/删除)与map(提供O(1)查找),并通过自定义entry结构体同步key-value与节点引用。
go 标准库没有内置 lru 缓存,但用 container/list + map 能高效实现,关键在于节点移动和哈希查找的协同——不是靠第三方库,而是控制指针和引用关系。
为什么不能只用 map 实现 LRU
map 本身无访问顺序,无法在 O(1) 时间内把某 key 对应的 value 移到“最近使用”位置;删除最久未用项时,遍历找最小时间戳是 O(n),违背 LRU 设计初衷。
- 加时间戳字段再排序?每次 Get/Write 都要更新+重排,性能崩盘
- 用切片维护顺序?删除中间项是 O(n),且 map 查不到对应切片索引
- 标准库
container/list提供双向链表,支持 O(1) 删除任意节点、O(1) 移动到头尾——这才是 LRU 的骨架
list.Element 和 map[string]*list.Element 怎么配对
核心设计:map 存 key → *list.Element,每个 Element.Value 是自定义结构体(含 key 和 value),这样 Get 时通过 map 快速定位节点,再从节点取值;Put 时若 key 已存在,直接 MoveToFront,否则新建节点并插入头部。
type entry struct {
key string
value interface{}
}
// 缓存结构体
type LRUCache struct {
list *list.List
m map[string]*list.Element
cap int
}
- 不要把
value直接塞进Element.Value:丢失 key 后无法从链表反查 map,导致 Delete 失效 - map 的 value 类型必须是
*list.Element,不是list.Element:需地址才能调用list.Remove或MoveToFront - Put 前检查容量:若
list.Len() == cap,先Remove(list.Back()),再从 map 中delete(m, e.Value.(*entry).key)
并发安全怎么加——别直接锁整个结构体
LRU 缓存读多写少,全局 sync.RWMutex 会成为瓶颈;更合理的是读写都用 sync.Mutex,但粒度可优化:只在涉及 list 修改和 map 更新的临界区加锁,Get 中的 map 查找本身是线程安全的(Go map 并发读不 panic,但读写混用会 crash)。
- 必须锁住所有涉及
list操作 +map增删改的地方:包括 Put 新 key、Hit 后 MoveToFront、Cap 满时 Evict - Get 可以先尝试无锁 map 查找,命中后再加锁做 MoveToFront;但要注意:如果 map 查到 element,但该 element 已被其他 goroutine 删除(比如正在 Evict),会导致 panic —— 所以实际必须锁住整个 Get 流程
- 简单起见,用一个
sync.Mutex保护list和map两个字段,比拆成两把锁更易验证正确性
真正容易出错的是 Evict 逻辑:删链表尾部节点后,必须同步从 map 中清除对应 key,漏掉就会造成“缓存泄露”——key 不再可达,但 map 还占着内存,GC 无法回收。这个点不在文档里,也不报错,只会在压测时悄悄吃光内存。


















