不能只用 raw map 做缓存,因为 Go 的 map 非并发安全,多 goroutine 读写会确定性 panic;不支持自动过期,易遗漏判断;无容量限制,导致内存无限增长和 OOM。

直接用 map + sync.RWMutex 就能跑通基础缓存逻辑,但不加过期控制、不处理内存增长、不考虑并发边界,上线后大概率会在高负载时 silently 失效或 OOM。
为什么不能只用 raw map 做缓存
Go 的 map 本身不是并发安全的——多个 goroutine 同时读写会触发 panic:「fatal error: concurrent map read and map write」。这不是概率问题,是确定性崩溃。
- 即使只读场景,若另一 goroutine 正在扩容
map(底层 hash table rehash),读操作也可能看到脏数据或 panic -
map不支持自动过期,time.Now().After(expireAt)这类判断必须由调用方每次手动做,极易遗漏 - 没有容量限制,key 持续写入会导致内存无限增长,GC 压力陡增,最终触发系统 OOM killer
最简可用缓存结构体怎么组织
一个真正能进测试环境的最小闭环缓存,至少得封装三件事:线程安全访问、过期时间判断、键存在性与值提取分离。别堆功能,先稳住这三点。
- 用
sync.RWMutex包裹map[string]cacheEntry,读用R Lock(),写用Lock() -
cacheEntry必须含value interface{}和expireAt time.Time,不能只存time.Duration—— 否则重启后所有相对过期时间全错乱 -
Get(key string)返回(interface{}, bool),bool 表示「键存在且未过期」,不是单纯 map 查找结果
示例核心字段:
立即学习“go语言免费学习笔记(深入)”;
type cacheEntry struct {
value interface{}
expireAt time.Time
}
<p>type SimpleCache struct {
mu sync.RWMutex
data map[string]cacheEntry
}
Set 和 Get 的过期逻辑怎么写才不出错
关键陷阱在于:过期判断必须在读取值的**同一把读锁内完成**,否则可能刚判断完未过期,下一毫秒就被另一个 goroutine 删除了 —— 出现「幻读」。
-
Set中若传入0或负数time.Duration,应视作「永不过期」,设expireAt为time.Time{}(零值)或time.Now().Add(100 * 365 * 24 * time.Hour)等远期时间,避免后续Before()判断异常 -
Get必须在mu.RLock()内同时完成 map 查找 +time.Now().Before(entry.expireAt)判断,不可拆成两步 - 不要在
Get里自动清理过期 key(即所谓 lazy delete)—— 这会升级为写锁,破坏读多写少场景下的性能优势
什么时候该放弃手写、转用第三方库
当你开始纠结这些问题时,说明已超出「学习逻辑」阶段,进入「工程可用」红线:
- 需要 LRU/LFU 淘汰策略,且要求
O(1)时间复杂度 —— 手写双向链表+map 容易出竞态,github.com/hashicorp/golang-lru更可靠 - 要支持分布式节点间一致性,或需对接 Redis 协议 —— 自研网络层和序列化成本远超收益
- 生产环境要求监控指标(命中率、平均延迟、内存占用曲线)—— 手写基本等于裸奔
手写缓存的价值从来不在“替代 Redis”,而在于看清 sync.RWMutex 怎么卡住并发、time.Time 零值怎么影响过期判断、以及为什么「存在」和「有效」是两个布尔状态 —— 这些细节,文档不会强调,但线上故障一定从这里爆。


















