不能在init()里初始化redis.Client,因为init()在包加载时自动执行,无法控制时机、注入配置或重试逻辑;一旦Redis地址错误、密码缺失或网络不通,程序直接panic退出,且与依赖注入、健康检查、配置热更新完全不兼容。

为什么不能在 init() 里初始化 redis.Client
因为 init() 是包加载时自动执行的,无法控制时机,更没法注入配置或重试逻辑。一旦 Redis 地址写死、密码缺失、网络不通,程序直接 panic 退出,连日志都来不及打。更重要的是,它和依赖注入、健康检查、配置热更新完全不兼容——比如你用 viper 读 YAML 配置,init() 执行时配置可能根本没加载完。
用 sync.Once + 懒加载函数封装单例客户端
核心是把初始化动作延迟到第一次 GetClient() 被调用时,并保证并发安全。不是“只执行一次”,而是“首次成功执行后,后续直接返回已建好的 client”。
-
sync.Once必须是包级变量,不能放在函数内或结构体字段里,否则每次调用都新建一个实例 - 初始化函数里必须做
rdb.Ping(ctx).Err()验证,否则连接失败会静默返回 nil client,后续所有Get/Set都 panic - 错误处理必须
panic(err),而不是返回 error —— 客户端不可用时,业务层继续跑等于埋雷 - 配置项(如
Addr、Password)必须从环境变量或配置中心读,例如os.Getenv("REDIS_ADDR")
var (
once sync.Once
rdb *redis.Client
rdbErr error
)
func GetClient() *redis.Client {
once.Do(func() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
rdb = redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_ADDR"),
Password: os.Getenv("REDIS_PASSWORD"),
DB: 0,
PoolSize: 30,
MinIdleConns: 5,
DialTimeout: 1*time.Second,
ReadTimeout: 1*time.Second,
WriteTimeout: 1*time.Second,
})
rdbErr = rdb.Ping(ctx).Err()
if rdbErr != nil {
panic(fmt.Sprintf("failed to connect to redis: %v", rdbErr))
}
})
if rdbErr != nil {
panic(rdbErr)
}
return rdb
}
HTTP handler 中怎么安全用这个懒加载 client
别在 handler 开头就调 GetClient() —— 如果并发高、Redis 刚启动未就绪,所有请求会卡在 once.Do 里排队,造成雪崩式延迟。正确做法是:服务启动时预热一次,确保 client 已 ready;handler 内只做业务操作。
- 启动阶段加预热:在
main()最后、server.ListenAndServe() 前,显式调一次GetClient() - handler 中所有操作必须带 context 超时:
ctx, cancel := context.WithTimeout(r.Context(), 200*time.Millisecond),不能用r.Context()直接传,避免上游超时被忽略 - 空值缓存统一用字符串
"null",别用nil或 JSON 的null,否则json.Unmarshal会报错 - 结构体字段必须导出且带
jsontag,否则json.Marshal出来是空对象
懒加载之后最容易漏掉的三件事
很多人以为封装了 sync.Once 就万事大吉,但线上故障往往出在这些“做完初始化就不管”的环节:
立即学习“go语言免费学习笔记(深入)”;
- 没关连接池:
defer GetClient().Close()必须在main()退出前注册,用signal.Notify捕获 SIGINT/SIGTERM - 没设
MinIdleConns:中间设备(如 SLB、iptables)会静默回收空闲连接,下次Get就卡住,必须设为 ≥5 - 没做 TTL 随机扰动:缓存集体过期会引发雪崩,
time.Duration(rand.Int63n(int64(2*time.Minute)))要在每次Set前重新生成,别用全局rand.Seed()
懒加载不是“躲开初始化”,而是把初始化时机、错误反馈、资源生命周期全部收归可控。真正难的从来不是写那一行 redis.NewClient,而是让这个 client 在容器重启、配置漂移、网络抖动、Redis 故障时,依然能给出确定性行为。


















