Go标准库无内置LRU缓存,需用container/list+map手写线程安全版本;关键在显式初始化map、entry结构体含key/value、锁粒度分离(Get用RLock,Put用Lock)、淘汰时先删map再删list、Value访问必加类型断言与ok判断。

Go 标准库没有内置 LRU 缓存,但用 container/list + map 手写一个线程安全、不 panic、容量可控的版本,50 行内就能搞定——关键不是“能不能写”,而是锁怎么加、节点怎么删、map 怎么和链表同步。
为什么不能直接用 container/list + map[string]interface{} 就完事?
常见错误是只存 value,不存 key,导致淘汰时无法反查 key 并从 map 中清理;或者没初始化 map,一写就 panic: assignment to entry in nil map;更隐蔽的是:多个 goroutine 同时 Get 同一个未命中 key,可能触发多次 MoveToFront,而 *list.Element 被并发修改,引发崩溃。
-
map必须显式make(map[string]*list.Element),不能只声明 - 每个
map的 value 必须是*list.Element,且该Element.Value必须是含key和value字段的结构体(比如type entry struct { key string; value interface{} }) -
list.Element本身不是线程安全的,哪怕只读e.Value,也必须在锁保护下进行——因为另一 goroutine 可能在你读之前已调用Remove,让e.Value变成nil
sync.RWMutex 锁粒度怎么设才不拖慢性能?
缓存读多写少,锁粗了就成瓶颈。把整个 Get 方法用 mu.Lock() 包住,所有读操作都会排队,违背缓存提速初衷。
-
Get用mu.RLock(),仅保护cache[key]查找 +list.MoveToFront()这两步 -
Put必须用mu.Lock(),因为涉及map写入、list.PushFront()、以及可能的RemoveOldest - 绝对不要在锁内做任何阻塞操作(如 HTTP 调用、
time.Sleep),缓存层只负责数据搬运
淘汰逻辑为什么必须“先删 map,再删 list”?
容量满时从 list.Back() 开始删,顺序错了就会出问题:如果先 list.Remove(e) 再 delete(cache, ent.key),e 被删后 e.Value 不再受 list 管理,但 Go 不会立即回收,此时若其他 goroutine 还拿着该指针做类型断言,可能读到脏数据或 panic。
立即学习“go语言免费学习笔记(深入)”;
- 正确顺序:取
e := l.list.Back()→ 断言ent := e.Value.(*entry)→delete(l.cache, ent.key)→l.list.Remove(e) - 结构体
entry必须同时含key和value,否则淘汰时无法反查 key 并清理map - 不要复用
node实例(比如清空字段重塞),容易引发并发或逻辑错乱;直接new更安全
类型断言怎么防静默丢数?
用 interface{} 存值看似灵活,实际是隐患源头:Put 时存 int,Get 时断言成 string,运行时不 panic,但返回零值,缓存“看起来生效了”,数据却丢了。
- 要么在
Get返回前强制加ok判断:if v, ok := e.Value.(string); ok { return v } - 要么业务确定只用一种类型(比如全是
[]byte),直接定义结构体字段为具体类型,省去断言开销和风险 - 别依赖
fmt.Sprintf("%v", v)做兜底:它掩盖底层类型差异,导致缓存穿透或脏数据
最易被忽略的点不是算法逻辑,而是 list.Element 的生命周期管理——它被 Remove 后,Value 字段就不再受保障,但 Go 不会立刻回收,容易误当成有效指针继续用。


















