sync.map仅适合读多写少、键基本不变且无需过期/淘汰的场景;用于高频写、需ttl或遍历时,性能与维护性均劣于sync.rwmutex+map。

Go 语言里缓存不是“加了就快”,而是得看用在哪、怎么清、谁来管——本地缓存没过期逻辑就是内存泄漏,Redis 缓存不设前缀就是数据互相踩,sync.Map 直接当通用缓存用可能比手写 map + sync.RWMutex 还慢。
sync.Map 当缓存用,什么时候会翻车
sync.Map 不是为通用缓存设计的:它没有过期机制、不支持统计、不能主动驱逐。高频写 + 低频读场景下,它的分段锁优势几乎没发挥空间;反而因内部指针跳转和延迟删除逻辑,比带读写锁的 map 多出不少间接开销。
- 只适合「写一次、读多次、永不过期」的配置类数据,比如服务启动时加载的白名单、路由表
- 一旦需要 TTL 或 LRU 驱逐,必须自己包装一层(比如加时间戳字段 + 定期 goroutine 扫描),但这样就失去
sync.Map的轻量优势 - 遍历性能差——
Range是快照语义,且无法保证顺序,不适合做缓存清理或监控采样
go-cache 的 NoExpiration 看似省事,实际埋雷
go-cache.New(cache.NoExpiration, cache.NoExpiration) 启动时不报错,跑几天后 RSS 内存持续上涨,pprof 一看全是 cache.item 占着不放——因为「永不过期」不等于「永不堆积」。
-
go-cache的清理依赖后台 goroutine 定期扫描,若设置NoExpiration,该 goroutine 实际上不会触发过期逻辑,只做空转 - 哪怕你手动调用
Delete,如果 key 量极大(比如用户 session ID 全塞进去),GC 压力会明显上升 - 正确做法是显式设一个合理
defaultExpiration(比如 10m),再配合业务逻辑在关键路径调用Set时传具体 TTL
bigcache 比 sync.Map 快,但只对特定数据有效
bigcache 的零 GC 和分片哈希确实快,但它要求 value 必须是 []byte,且 key 最好是短字符串(json.Marshal 抹平,甚至更慢。
- 适合缓存 API 响应体、模板渲染结果等「纯字节流」数据
- 不建议缓存数据库查询结果结构体(如
*User),除非你用gob或msgpack且确认反序列化开销可控 - 初始化时
shards数量别盲目设大(默认 256),超过 CPU 核数太多反而增加调度开销;建议从runtime.NumCPU()开始调优
本地缓存 + Redis 双写,一致性不是靠「先删 Redis 再写 DB」能解决的
常见错误是:DB 更新成功后,立刻 DEL Redis key,再顺手清掉本地缓存——但本地缓存是进程级的,其他实例根本不知道这波操作。
- 多实例部署时,本地缓存只能靠「超时被动失效」,不能依赖主动清除;否则必然出现短暂脏读
- 如果业务允许秒级不一致,就老老实实设短 TTL(比如 30s),别搞复杂同步
- 真要强一致,得引入消息队列(如 Kafka)广播缓存失效事件,或者用 Redis 的
PUB/SUB,但要注意网络分区时的消息丢失风险
最常被忽略的一点:缓存不是性能银弹,而是放大器——它会把慢查询、N+1 问题、序列化瓶颈原样放大十倍。上线前务必用 go tool pprof 对比加缓存前后的 allocs/sec 和 gc pause,而不是只看接口 P95 延迟。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











