基于Go的缓存实现

Go语言教程 Go语言教程
2025年10月27日 13:19

有时候啊,你写个服务跑得好好的,结果一查日志发现数据库被打爆了,QPS 一高延迟直接飙上去。这时候你就会想,要不我加个缓存试试?

没错, 缓存 基本是性能优化里最简单粗暴但最有效的手段。尤其在 Go 项目里,自己写个轻量级缓存比引入 Redis 那种外部服务方便太多。下面咱就来聊聊在 Go 里怎么实现一个本地缓存,够用、稳定,还能支持过期清理。

最简单的内存缓存

最初的想法很简单,用个 map 存 key-value 就完事了:

package main

import (
 "fmt"
)

func main() {
 cache := make(map[string]string)
 cache["user_1"] = "Tom"
 cache["user_2"] = "Jerry"

 fmt.Println(cache["user_1"]) // 输出 Tom
}

看似没问题,但这个实现只能在单线程下跑,一旦有并发访问,比如多个协程同时读写 map,Go 直接给你报 fatal error。 所以第一个坑: map 不是线程安全的

加上锁,稳一点

那咋办?加锁呗,用 sync.RWMutex 就行。这样我们可以同时读,但写的时候要锁定整个 map。

package main

import (
 "fmt"
 "sync"
)

type Cache struct {
 data map[string]string
 mu   sync.RWMutex
}

func NewCache() *Cache {
 return &Cache{data: make(map[string]string)}
}

func (c *Cache) Set(key, value string) {
 c.mu.Lock()
 defer c.mu.Unlock()
 c.data[key] = value
}

func (c *Cache) Get(key string) (stringbool) {
 c.mu.RLock()
 defer c.mu.RUnlock()
 val, ok := c.data[key]
 return val, ok
}

func main() {
 cache := NewCache()
 cache.Set("user_1""Tom")
 cache.Set("user_2""Jerry")

 if v, ok := cache.Get("user_1"); ok {
  fmt.Println(v)
 }
}

这个版本已经能在多协程下用了,但问题也明显:

  • 没有过期时间。
  • 数据越堆越多,内存迟早爆。

给缓存加上过期机制

我们想要的效果是:写入时可以设置过期时间,到期自动清理掉。 实现思路也简单,每条数据记录它的过期时间,然后用一个后台协程定期清理过期的 key。

package main

import (
 "fmt"
 "sync"
 "time"
)

type item struct {
 value      string
 expireTime int64
}

type Cache struct {
 data map[string]item
 mu   sync.RWMutex
}

func NewCache() *Cache {
 c := &Cache{data: make(map[string]item)}
 go c.cleanup() // 启动清理协程
 return c
}

func (c *Cache) Set(key, value string, duration time.Duration) {
 c.mu.Lock()
 defer c.mu.Unlock()
 c.data[key] = item{
  value:      value,
  expireTime: time.Now().Add(duration).UnixNano(),
 }
}

func (c *Cache) Get(key string) (stringbool) {
 c.mu.RLock()
 defer c.mu.RUnlock()
 it, ok := c.data[key]
 if !ok || time.Now().UnixNano() > it.expireTime {
  return ""false
 }
 return it.value, true
}

func (c *Cache) cleanup() {
 for {
  time.Sleep(5 * time.Second)
  now := time.Now().UnixNano()
  c.mu.Lock()
  for k, v := range c.data {
   if now > v.expireTime {
    delete(c.data, k)
   }
  }
  c.mu.Unlock()
 }
}

func main() {
 cache := NewCache()
 cache.Set("session_123""userA"3*time.Second)
 time.Sleep(2 * time.Second)

 if v, ok := cache.Get("session_123"); ok {
  fmt.Println("命中:", v)
 }

 time.Sleep(2 * time.Second)
 if _, ok := cache.Get("session_123"); !ok {
  fmt.Println("过期啦")
 }
}

输出:

命中: userA
过期啦

这下基本满足大部分轻量级缓存需求了。 但再往深想一点,你会发现清理线程其实有点“粗暴”——它每次都全量扫描整个 map。数据量大了,性能就废了。

优化一下清理策略

如果你的数据量很大,比如上万个 key,可以优化成惰性清理(lazy eviction): 只有在调用 Get() 的时候才去检查是否过期,过期就删。 再配合定时器定期小范围扫描,这样性能更均衡。

想偷懒?可以直接用 bigcache ristretto

如果你的项目已经上生产,建议别造轮子了。 Go 社区里有很多成熟的缓存库,比如:

  • allegro/bigcache (无锁高性能缓存)
  • dgraph-io/ristretto (支持 LFU 驱逐策略)

使用起来特别简单,比如 bigcache

package main

import (
 "fmt"
 "time"

 "github.com/allegro/bigcache"
)

func main() {
 cache, _ := bigcache.NewBigCache(bigcache.DefaultConfig(10 * time.Second))
 cache.Set("foo", []byte("bar"))

 entry, _ := cache.Get("foo")
 fmt.Println(string(entry))
}

这种方案性能非常强,内部做了分片、无锁读写、并发清理,非常适合高 QPS 的场景。

小结

在 Go 里实现缓存其实不难,关键在于你要想清楚需求:

  • 是单机缓存还是分布式缓存?
  • 要不要支持过期?
  • 需不需要淘汰策略(LRU、LFU)?

如果只是临时加速数据访问、减少数据库压力,本地缓存是极好的选择; 如果你要多节点共享缓存,那 Redis、Memcached 还是更靠谱。

写 Go 服务的时候,我一般是先加个本地缓存,跑稳定了再考虑 Redis。 因为缓存这玩意嘛,简单才是第一生产力。

-END -

我为大家打造了一份RPA教程, 完全免费: songshuhezi.com/rpa.html


🔥 虎哥私藏精品 🔥

虎哥作为一名老码农,整理了全网最全 GO后端开发资料合集 》。 总量高达 650GB ,点击下方公众号回复关键字 go 全部免费领取