拒绝缓存硬编码!用go-cache实现Go内存与Redis的无缝切换

admin 2026-06-30 07:34:29 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 文档针对Go项目中缓存硬编码问题提出解决方案,通过定义统一Cache接口实现内存缓存(go-cache)与Redis的无缝切换。核心要点包括:抽象缓存层隔离业务逻辑与具体实现、规范缓存键命名与序列化处理、配置驱动缓存类型选择。关键实践涉及错误处理策略(读缓存失败不阻断主流程)、类型安全校验及版本化key管理,最终达成代码可维护性与环境适配性的提升。 综合评分: 82 文章分类: 解决方案,安全开发,应用安全


cover_image

拒绝缓存硬编码!用 go-cache 实现 Go 内存与 Redis 的无缝切换

原创

go go

Go语言教程

2026年6月27日 13:38 陕西

在小说阅读器读本章

去阅读

拒绝缓存硬编码!用 go-cache 实现 Go 内存与 Redis 的无缝切换

缓存代码最怕这种写法:业务里到处都是 redis.Get,本地跑不起来,单测要起 Redis,临时想换成本地缓存还得全局搜索。

我看到这种代码,第一反应不是 Redis 慢不慢,而是这玩意后面一定不好改。

比如用户信息接口里,经常会写成这样:

func GetUserName(ctx context.Context, userID int64) (string, error) {
 key := fmt.Sprintf("user:name:%d", userID)

 val, err := redisClient.Get(ctx, key).Result()
 if err == nil {
  return val, nil
 }

 name, err := queryUserNameFromDB(ctx, userID)
 if err != nil {
  return "", err
 }

 _ = redisClient.Set(ctx, key, name, 10*time.Minute).Err()
 return name, nil
}

看着没啥问题。

线上能跑,缓存也生效。

但这段代码有个很别扭的地方:业务函数知道了 Redis 的存在。

今天你用 Redis,明天本地开发想用内存缓存,压测时想把缓存关掉,单测想塞一个假缓存,都得改业务代码。

这就不对了。

缓存应该像一层抽屉,业务只管拿和放,不该关心抽屉后面是内存,还是 Redis,还是以后换成别的东西。

我一般会先把缓存接口抽出来,别一上来就封装一大坨。

package cachex

import (
 "context"
 "time"
)

type Cache interface {
 Get(ctx context.Context, key string) (string, bool)
 Set(ctx context.Context, key string, val string, ttl time.Duration) error
 Del(ctx context.Context, key string) error
}

注意这里 Get 返回的是 (string, bool)

我不太喜欢业务层去判断 Redis 的 nil 错误,也不喜欢把缓存未命中写成异常。缓存没命中就是没命中,别装成事故。

内存缓存这里可以用 github.com/patrickmn/go-cache

package cachex

import (
 "context"
 "time"

 gocache "github.com/patrickmn/go-cache"
)

type LocalCache struct {
 box *gocache.Cache
}

func NewLocalCache(defaultTTL time.Duration) *LocalCache {
 return &LocalCache{
  box: gocache.New(defaultTTL, time.Minute),
 }
}

func (c *LocalCache) Get(ctx context.Context, key string) (string, bool) {
 v, ok := c.box.Get(key)
 if !ok {
  return "", false
 }

 s, ok := v.(string)
 if !ok {
  _ = c.Del(ctx, key)
  return "", false
 }

 return s, true
}

func (c *LocalCache) Set(ctx context.Context, key string, val string, ttl time.Duration) error {
 c.box.Set(key, val, ttl)
 return nil
}

func (c *LocalCache) Del(ctx context.Context, key string) error {
 c.box.Delete(key)
 return nil
}

这里有个小细节。

内存缓存里取出来的是 interface{},不要偷懒直接断言。类型不对就删掉,别让脏值在里面继续晃。

Redis 版本也按同一个接口来。

package cachex

import (
 "context"
 "errors"
 "time"

 "github.com/redis/go-redis/v9"
)

type RedisCache struct {
 rdb *redis.Client
}

func NewRedisCache(addr, password string, db int) *RedisCache {
 return &RedisCache{
  rdb: redis.NewClient(&redis.Options{
   Addr:         addr,
   Password:     password,
   DB:           db,
   DialTimeout:  800 * time.Millisecond,
   ReadTimeout:  1200 * time.Millisecond,
   WriteTimeout: 1200 * time.Millisecond,
  }),
 }
}

func (c *RedisCache) Get(ctx context.Context, key string) (string, bool) {
 val, err := c.rdb.Get(ctx, key).Result()
 if errors.Is(err, redis.Nil) {
  return "", false
 }
 if err != nil {
  return "", false
 }
 return val, true
}

func (c *RedisCache) Set(ctx context.Context, key string, val string, ttl time.Duration) error {
 return c.rdb.Set(ctx, key, val, ttl).Err()
}

func (c *RedisCache) Del(ctx context.Context, key string) error {
 return c.rdb.Del(ctx, key).Err()
}

Redis 这里我刻意没有把错误继续往上抛。

原因很简单,读缓存失败,不该把主流程打死。

但 Set 和 Del 我保留了错误,方便上层打日志。缓存写失败可以忍,完全没日志就有点瞎了。

再做一个构造器,用配置决定当前用哪种缓存。

package cachex

import (
 "time"
)

type Config struct {
 Driver string

 RedisAddr     string
 RedisPassword string
 RedisDB       int
}

func NewCache(conf Config) Cache {
 switch conf.Driver {
 case "redis":
  return NewRedisCache(conf.RedisAddr, conf.RedisPassword, conf.RedisDB)
 case "local":
  return NewLocalCache(5 * time.Minute)
 default:
  return NewLocalCache(time.Minute)
 }
}

配置可以长这样:

cache:
  driver: local
  redis_addr: 127.0.0.1:6379
  redis_db: 0

开发环境用 local

测试环境想少依赖 Redis,也用 local

线上要共享缓存,再切 redis

业务代码这边就干净多了。

type UserService struct {
 cache cachex.Cache
}

func NewUserService(c cachex.Cache) *UserService {
 return &UserService{cache: c}
}

func (s *UserService) GetUserName(ctx context.Context, userID int64) (string, error) {
 key := fmt.Sprintf("user:name:%d", userID)

 if name, ok := s.cache.Get(ctx, key); ok {
  return name, nil
 }

 name, err := queryUserNameFromDB(ctx, userID)
 if err != nil {
  return "", err
 }

 if err := s.cache.Set(ctx, key, name, 10*time.Minute); err != nil {
  log.Printf("cache set failed key=%s err=%v", key, err)
 }

 return name, nil
}

这时候业务层已经不知道缓存后面是谁了。

它只知道:先查缓存,没命中查库,查完回填。

这个顺序别写反,也别为了所谓“优雅”把缓存回填藏到很深的 helper 里。线上排查时,最烦的就是一个缓存到底写没写,要跳五六层代码才能看到。

如果缓存的是结构体,我也建议在缓存层外面就序列化清楚,别把各种对象直接塞进 go-cache

type UserProfile struct {
 ID   int64  `json:"id"`
 Name string `json:"name"`
 Vip  bool   `json:"vip"`
}

func encodeProfile(p UserProfile) (string, error) {
 b, err := json.Marshal(p)
 if err != nil {
  return "", err
 }
 return string(b), nil
}

func decodeProfile(s string) (UserProfile, error) {
 var p UserProfile
 err := json.Unmarshal([]byte(s), &p)
 return p, err
}

原因也不复杂。

内存缓存可以存对象,Redis 只能存字符串。你现在为了省事直接塞对象,后面一切 Redis,又要改业务。

这种坑我见过不止一次。

还有一个现场里容易漏的点:缓存 key 不要散在业务里到处拼。

func userNameKey(userID int64) string {
 return fmt.Sprintf("user:name:v1:%d", userID)
}

加个版本号很土,但好用。

字段结构变了,缓存内容不兼容了,直接升 v2,不用半夜连 Redis 手动删 key。

内存缓存和 Redis 无缝切换,关键不在工具本身,而在边界。

go-cache 只是本地实现,Redis 只是远端实现。真正要避开的,是业务代码和某个缓存组件绑死。

代码里少一个硬编码,后面排障就少一个死结。缓存这种东西,越早把口子收住,后面越省事。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:Go语言教程 go go《拒绝缓存硬编码!用 go-cache 实现 Go 内存与 Redis 的无缝切换》

评论:0   参与:  0