文章总结: 文档针对Go项目中缓存硬编码问题提出解决方案,通过定义统一Cache接口实现内存缓存(go-cache)与Redis的无缝切换。核心要点包括:抽象缓存层隔离业务逻辑与具体实现、规范缓存键命名与序列化处理、配置驱动缓存类型选择。关键实践涉及错误处理策略(读缓存失败不阻断主流程)、类型安全校验及版本化key管理,最终达成代码可维护性与环境适配性的提升。 综合评分: 82 文章分类: 解决方案,安全开发,应用安全
拒绝缓存硬编码!用 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 的无缝切换》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论