← 返回博客
go2026-09-12 22:07:362 分钟 · 542 0

Gin 配置热加载与特性开关

用 viper 监听配置文件变化做热加载,再用特性开关(feature flag)按用户灰度新功能,不发版就能开关功能。

#gin#配置#特性开关

改个配置要重启服务,灰度新功能要发版,都慢。配置热加载让改配置即时生效,特性开关让新功能按用户比例放开,回滚只是改个值。这一篇两件事都做。

1. 为什么热加载

日志级别、限流阈值、下游地址,这些运行时常调。每次改都重启,连接会闪断、正在处理的请求丢失(第 8 篇讲的优雅停机也救不了频繁重启)。

2. viper 监听变更

第 8 篇用 viper 读了配置,加 WatchConfig 就能在文件变化时回调:

v.WatchConfig()
v.OnConfigChange(func(e fsnotify.Event) {
    v.Unmarshal(&cfg)
    log.Printf("配置已热加载: %s", e.Name)
})

cfg 是全局指针,回调里重新 Unmarshal 覆盖。注意并发:读 cfg 的地方要用 atomic 或 rwlock,别让热加载和请求抢同一个字段。

3. 热加载要原子的

配置结构放 atomic.Value,回调整体替换,读方无锁:

var cfgAtomic atomic.Value

func SetCfg(c *Config) { cfgAtomic.Store(c) }
func GetCfg() *Config { return cfgAtomic.Load().(*Config) }

读配置 GetCfg() 永远拿到完整一份,不会出现改到一半读到半新半旧。

4. 特性开关控制新功能

新功能不敢全量放,先给内部用户开。开关放配置或独立存储:

func FeatureOn(name, uid string) bool {
    f, ok := GetCfg().Features[name]
    if !ok || !f.Enabled {
        return false
    }
    if f.Allowlist[uid] {
        return true
    }
    h := fnvHash(uid) % 100
    return h < f.RolloutPct
}

5. 中间件按开关分流

新接口旧逻辑并存,靠开关决定走哪条:

func FeatureGate(name string) gin.HandlerFunc {
    return func(c *gin.Context) {
        uid := c.GetString("uid")
        if !FeatureOn(name, uid) {
            c.JSON(404, gin.H{"error": "功能未开放"})
            c.Abort()
            return
        }
        c.Next()
    }
}

开关关了返回 404,对未放行用户无感;出了问题把 RolloutPct 调 0 立即全关,不用发版。

6. 开关放哪

简单场景放 viper 配置(跟着热加载走)。多服务共享、要运营后台实时改,放 Redis 或专用 flag 服务(如 Unleash),别散落在各服务配置文件里。

7. 上线清单

热加载的配置结构用 atomic 或锁保护,避免读到半更新状态。改配置后打日志,方便追溯。特性开关默认关,灰度从小比例起,回滚改值不发包。开关判断失败走保守路径(关),别因为开关服务挂了就全开。

下一篇讲灰度发布,把流量按规则分到新旧版本。

相关推荐

本文为原创文章,采用CC BY-NC-SA 4.0协议授权,转载请保留署名与原文链接。原文链接:https://www.wxbuluo.com/article/195