- 缓存
【免费下载链接】go-cache
An in-memory key:value store/cache (similar to Memcached) library for Go, suitable for single-machine applications.
go-cache 是一个面向单机应用的 Go 语言内存键值缓存库,本质上是一个带过期时间的线程安全map[string]interface{},与 memcached 类似的语义但完全运行在进程内,无需网络序列化与传输。读完本文,你将掌握 go-cache 的安装方式、全部核心 API(Set/Get/Add/Replace/Increment 等)、过期清理与持久化机制,并能结合源码理解其并发设计与适用边界。
go-cache 是什么:单机场景下的轻量缓存方案
go-cache 是类似 memcached 的 in-memory key:value 存储/缓存库,专门为运行在单台机器上的应用设计。它的最大优势在于:由于本质上就是一个带过期时间的线程安全map[string]interface{},不需要对内容进行序列化,也不需要通过网络传输,读写都在本进程内存中直接完成。
这一点与 memcached、Redis 等独立缓存服务形成鲜明对比——后两者需要客户端与服务端之间进行网络往返、序列化/反序列化,而 go-cache 直接作为库嵌入应用进程,省去了这一层开销。
go-cache 的核心能力可归纳为三点:
- 任意对象可存储:缓存值类型为
interface{},可以是字符串、整数、结构体、切片乃至任意自定义类型; - 灵活的过期策略:每个条目可指定独立过期时长,也可设为永久有效;
- 多 goroutine 安全:内部使用
sync.RWMutex保证并发读写安全,可放心地被多个 goroutine 共享使用。
需要明确的是,go-cache不适用于作为持久化数据存储。不过,整个缓存可以保存到文件或从文件加载(详见后文持久化章节),用于在宕机后快速恢复现场。
安装
使用标准的 Go 模块工具安装即可:
go get github.com/patrickmn/go-cache导入路径为:
import ( "github.com/patrickmn/go-cache" )快速上手:核心 API 实战
下面这段代码(源自 README.md 的 Usage 示例)演示了 go-cache 最核心的用法:创建缓存、写入带过期时间的值、写入永久值、读取并做类型断言。
package main import ( "fmt" "github.com/patrickmn/go-cache" "time" ) func main() { // 创建一个默认过期时间为 5 分钟、每 10 分钟清理一次过期条目的缓存 c := cache.New(5*time.Minute, 10*time.Minute) // 以默认过期时间写入 key "foo",值为 "bar" c.Set("foo", "bar", cache.DefaultExpiration) // 写入 key "baz" 值为 42,永不过期 // (该条目在重新 Set 或调用 c.Delete("baz") 之前不会被移除) c.Set("baz", 42, cache.NoExpiration) // 读取 key "foo" 关联的字符串 foo, found := c.Get("foo") if found { fmt.Println(foo) } // Go 是静态类型语言,而缓存值可以是任意类型(interface{}), // 因此将值传给不接受任意类型的函数时,必须做类型断言。 // 对于只使用一次的值,最简方式是: foo, found := c.Get("foo") if found { MyFunction(foo.(string)) } // 如果同一个值要在函数内多次使用,下面两种写法更合适: if x, found := c.Get("foo"); found { foo := x.(string) // ... } // 或者 var foo string if x, found := c.Get("foo"); found { foo = x.(string) } // ... // foo 之后可以自由地作为 string 传递 // 追求性能?存储指针! c.Set("foo", &MyStruct, cache.DefaultExpiration) if x, found := c.Get("foo"); found { foo := x.(*MyStruct) // ... } }关于类型断言需要特别注意:由于Get返回的是interface{},把它传给MyFunction(string)这类要求具体类型的函数时必须显式断言。存储指针(而非值拷贝)是一种常见的性能优化手段——指针的断言与传递代价更低,且多个读取方看到的是同一份数据(参见 cache_test.go 中的TestStorePointerToStruct,它验证了通过指针修改结构体后再次Get能读到更新后的值)。
数据模型:Item 与两个关键常量
深入源码 cache.go,可以看到缓存条目的定义与两个核心常量:
type Item struct { Object interface{} Expiration int64 } // 返回条目是否已过期 func (item Item) Expired() bool { if item.Expiration == 0 { return false } return time.Now().UnixNano() > item.Expiration } const ( // 用于需要传入过期时间参数的函数:永不过期 NoExpiration time.Duration = -1 // 用于需要传入过期时间参数的函数:等价于传入 New() 或 NewFrom() // 创建缓存时给定的默认过期时长(例如 5 分钟) DefaultExpiration time.Duration = 0 )关键点解读:
Item.Expiration是一个 int64 类型的UnixNano 时间戳(绝对过期时刻),而非剩余时长。当值为 0 时表示不过期;NoExpiration = -1:表示该条目永不过期,直到被重新写入或删除;DefaultExpiration = 0:表示使用创建缓存时New()/NewFrom()传入的默认过期时长。
底层缓存结构(cache.go)为:
type cache struct { defaultExpiration time.Duration items map[string]Item mu sync.RWMutex onEvicted func(string, interface{}) janitor *janitor }而对外暴露的Cache类型(cache.go)只是简单内嵌*cache,用来承载 janitor 清理 goroutine 的 finalizer,避免内部 goroutine 阻碍垃圾回收(详见「过期清理机制」小节)。
完整 API 详解
创建缓存:New 与 NewFrom
func New(defaultExpiration, cleanupInterval time.Duration) *Cache- defaultExpiration:条目默认过期时长。如果小于 1(或传
NoExpiration),则条目默认永不过期,必须手动删除; - cleanupInterval:清理间隔。如果小于 1,则过期条目不会自动清理,只能手动调用
c.DeleteExpired()。
NewFrom的签名与New相同,但额外接受一个items map[string]Item作为底层存储,适用于从反序列化的缓存起步,或传入make(map[string]Item, 500)这类预分配 map 来提升缓存将达较大规模时的启动性能(见 cache.go 的文档注释)。
写入:Set、SetDefault、Add、Replace
| 方法 | 行为 |
|---|---|
Set(k, x, d) | 写入条目,覆盖已存在的同名 key(cache.go) |
SetDefault(k, x) | 等价于Set(k, x, DefaultExpiration)(cache.go) |
Add(k, x, d) | 仅当 key 不存在(或已过期)时写入,否则返回错误Item %s already exists(cache.go) |
Replace(k, x, d) | 仅当 key 已存在且未过期时更新,否则返回错误Item %s doesn't exist(cache.go) |
Set的内部实现值得注意:d == DefaultExpiration时使用缓存的默认过期时长;d > 0时计算time.Now().Add(d).UnixNano()作为绝对过期时间戳;d <= 0(即NoExpiration)时过期时间戳保持 0,表示永不过期。源码中的 TODO 注释还提到,mu.Unlock没有使用defer,因为 defer 在 go1 时代会带来约 200ns 的额外开销——这是为了极致性能做的微优化。
TestAdd与TestReplace(cache_test.go)验证了这两个方法的"条件写入"语义:对已存在的 key 再次Add会返回错误,而对不存在的 key 执行Replace同样返回错误。
读取:Get 与 GetWithExpiration
func (c *cache) Get(k string) (interface{}, bool) func (c *cache) GetWithExpiration(k string) (interface{}, time.Time, bool)Get返回值(找不到时为 nil)和一个 bool 表示 key 是否存在。注意:过期但尚未被清理的条目,Get同样会返回nil, false——读取时就会做惰性过期判断(cache.go),不必等待 janitor 清理;GetWithExpiration额外返回条目的绝对过期时间;若条目永不过期,返回零值time.Time{}(cache.go)。
删除与清空:Delete、DeleteExpired、Flush
| 方法 | 行为 |
|---|---|
Delete(k) | 删除指定 key,不存在时静默无操作;若注册了OnEvicted回调则触发之(cache.go) |
DeleteExpired() | 删除所有已过期条目,并逐个触发OnEvicted回调(cache.go) |
Flush() | 清空整个缓存(cache.go) |
统计:ItemCount 与 Items
func (c *cache) ItemCount() int // 当前条目数,可能包含已过期但未清理的条目([cache.go](https://link.gitcode.com/i/1ae509c563fac7922f889db972e766aa)) func (c *cache) Items() map[string]Item // 返回所有未过期条目的副本 map([cache.go](https://link.gitcode.com/i/6d50049e921f87f6661e97ba7ab74c08))Items()会过滤掉已过期条目并返回一份新 map,是持久化流程的关键入口。
数值原子操作:Increment 与 Decrement
go-cache 内置了对缓存数值进行增减操作的方法,避免"读-改-写"三步之间的竞态(因为整个操作在持有锁的状态下完成):
- 通用方法:
Increment(k, n int64)、IncrementFloat(k, n float64)、Decrement(k, n int64)、DecrementFloat(k, n float64)——内部通过 type switch 支持int、int8、int16、int32、int64、uint、uintptr、uint8、uint16、uint32、uint64、float32、float64等全部数值类型(cache.go); - 专用方法:
IncrementInt64(k, n)、DecrementUint32(k, n)等针对单一类型,直接返回增减后的值,省去再次Get并断言的步骤(cache.go)。
使用注意事项:通用方法对类型不匹配的值(例如对 string 调用Increment)会返回错误;溢出/下溢时遵循 Go 原生语义(TestIncrementOverflowInt验证了int8(127)+1会翻转为-128,见 cache_test.go)。
驱逐回调:OnEvicted
func (c *cache) OnEvicted(f func(string, interface{}))注册一个回调函数,当条目被驱逐(手动Delete、DeleteExpired清理,注意覆盖写入不算驱逐)时以 key 和 value 为参数调用。传nil可取消回调(cache.go)。TestOnEvicted(cache_test.go)验证了回调会在Delete时被触发。
过期清理机制:janitor 背后的原理
创建缓存时传入的cleanupInterval决定了一个后台 goroutine(janitor,意为"清洁工")的清理节奏。核心实现在 cache.go:
func (j *janitor) Run(c *cache) { ticker := time.NewTicker(j.Interval) for { select { case <-ticker.C: c.DeleteExpired() case <-j.stop: ticker.Stop() return } } }这里有一个非常巧妙的工程细节:janitor goroutine 会永久运行DeleteExpired,如果不加处理,它会阻止外层Cache对象被垃圾回收。go-cache 的解决方案是——通过runtime.SetFinalizer设置终结器:当外层Cache被 GC 回收时,finalizer 向 janitor 的 stop channel 发送信号,使 goroutine 退出,进而让内部cache也可以被回收(cache.go 的注释完整解释了这一设计)。
需要厘清的是:过期判断是"惰性 + 定期清理"双轨制。即使 janitor 尚未清理,Get在读取时也会检查时间戳并返回"未找到",所以业务层拿不到已过期的值;janitor 负责的是物理删除,防止过期条目无限累积占用内存。
持久化:Save/Load 与 Items()+NewFrom()
go-cache 虽然定位是纯内存缓存,但提供了两条持久化路径用于宕机快速恢复:
(一)旧方案:Save / Load(已标记弃用)
func (c *cache) Save(w io.Writer) error // 用 gob 编码整个 items map func (c *cache) SaveFile(fname string) error // 保存到文件 func (c *cache) Load(r io.Reader) error // 从流加载(跳过已存在且未过期的 key) func (c *cache) LoadFile(fname string) error // 从文件加载源码注释明确指出这些方法已弃用,推荐使用下述的Items()+NewFrom()组合(cache.go)。TestCacheSerialization和TestFileSerialization(cache_test.go)覆盖了这两套流程,包括指针、切片、嵌套结构体等复杂类型。
(二)推荐方案:Items() + NewFrom()
按 README.md 的指引,标准做法是:
- 用
c.Items()取出未过期条目 map; - 用任意序列化手段(如
gob.Encode)把该 map 写入文件; - 重启后用
NewFrom()基于反序列化得到的 map 重建缓存。
使用NewFrom有几点注意事项(cache.go 文档明确列出):
- 传入的 map 将成为缓存的底层存储,只有缓存的方法会同步访问它,因此创建后不要保留对该 map 的引用,后续通过
c.Items()访问(同样受此约束); - 使用 gob 序列化时,编码前必须对
c.Items()拿到的 map 中存储的各类型调用gob.Register()注册,解码前也要注册同样的类型,否则自定义类型会无法编码/解码。
并发安全与性能注意事项
go-cache 的并发安全建立在sync.RWMutex之上:读操作(Get、Items、ItemCount等)使用RLock,写操作(Set、Add、Replace、Delete、Increment等)使用Lock(见 cache.go 的结构定义及各方法实现)。RWMutex 允许大量并发读,只有写操作才会互斥,非常适合"读多写少"的缓存典型负载。
值得留意的性能细节:
Set的源码中刻意内联了set逻辑(避免一次函数调用),并放弃defer Unlock以节省约 200ns(cache.go 注释);Get同样内联了get和过期判断,并在未命中时立即解锁返回;- 由于值类型是
interface{},Get拿到的都是接口引用而非拷贝;存储指针可进一步降低断言与传递开销; - 仓库内置的基准测试(cache_test.go)包含
BenchmarkCacheGetExpiring、BenchmarkCacheGetConcurrentExpiring等,读者可在自己机器上运行go test -bench=.获取本机数据,具体数值随硬件与 Go 版本而异。
实验性扩展:分片缓存(sharded.go)
仓库中还包含一个实验性且未导出的分片缓存实现(sharded.go)。它的动机是:标准缓存在新增条目时需要对整个缓存加写锁,而分片缓存通过把条目分散到多个独立桶(每个桶是一个独立的*cache),让写锁只作用于单个桶,从而改善大规模缓存下的算法复杂度。
实现要点:
- 使用 djb2 变体哈希函数
djb33(源码注释称其比带hash.Hash开销的 FNV 快 5 倍,见 sharded.go); - 哈希种子取自系统 CSPRNG(
crypto/rand),若读取失败会向 stderr 输出安全警告并回退到不安全的math/rand(sharded.go); - 桶选择公式为
djb33(seed, k) % m(sharded.go)。
源码注释(sharded.go)坦诚地给出了其当前状态:截至编写时,在总缓存规模较小时,桶选择的额外开销使操作比标准缓存慢约一倍,而缓存规模较大时反而更快。因此它是面向未来优化的实验代码,目前未对外导出,普通使用者应使用标准New()/NewFrom()路径。
总结与适用边界
| 适用场景 | 不适用场景 |
|---|---|
| 单机应用的内存缓存(配置、会话、热点数据等) | 多实例/分布式共享缓存(无网络协议) |
| 多 goroutine 并发读写 | 需要跨进程或跨机器访问 |
| 带过期时间的临时数据 | 需要持久化的业务数据(持久化仅用于宕机恢复) |
| 快速实现类似 memcached 语义而无需部署服务端 | 需要 LRU/LFU 等高级淘汰策略(go-cache 仅按时间过期) |
作为一篇完整的参考资料,本文覆盖了 go-cache 从安装、基础读写、条件写入、数值操作、过期清理、持久化到并发模型的全部核心知识。继续深入可研读仓库源码:cache.go(核心实现,1161 行)、sharded.go(实验性分片实现)、cache_test.go(覆盖各 API 语义的完整测试与基准)、sharded_test.go(分片缓存测试与基准),以及 README.md 中经测试验证的官方用法示例。
- 缓存
【免费下载链接】go-cache
An in-memory key:value store/cache (similar to Memcached) library for Go, suitable for single-machine applications.
相关推荐
go-cache 内存缓存库完全指南:在 orchestrator 中的并发安全实践
go cache 内存缓存库完全指南:在 orchestrator 中的并发安全实践 导读 go cache https://link.gitcode.com/
后端数据库高性能Go语言Redis缓存库推荐:go-redis/cache
高性能Go语言Redis缓存库推荐:go redis/cache 在现代应用开发中,缓存是提升系统性能的关键手段之一。特别是在高并发场景下,合理使用缓存可以显著
Go 缓存库 `go-cache` 教程
Go 缓存库 go cache 教程 1. 项目介绍 go cache 是一个适用于单机应用程序的内存键值存储/缓存库,类似于 memcached。它提供了一个
缓存
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考