实现配置热更新
问题
如何在 Go 程序运行时动态更新配置而不重启服务?
答案
方案一:fsnotify 监听文件变化
import "github.com/fsnotify/fsnotify"
type Config struct {
Port int `yaml:"port"`
LogLevel string `yaml:"log_level"`
RateLimit int `yaml:"rate_limit"`
}
// 用 atomic.Value 保证读取无锁
var globalConfig atomic.Value
func LoadConfig(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return err
}
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return err
}
globalConfig.Store(&cfg)
log.Printf("配置已加载: %+v", cfg)
return nil
}
func GetConfig() *Config {
return globalConfig.Load().(*Config)
}
// 监听配置文件变化
func WatchConfig(path string) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
go func() {
defer watcher.Close()
for {
select {
case event := <-watcher.Events:
if event.Op&fsnotify.Write == fsnotify.Write {
log.Println("配置文件变更,重新加载...")
if err := LoadConfig(path); err != nil {
log.Printf("重新加载失败: %v", err)
}
}
case err := <-watcher.Errors:
log.Printf("watcher 错误: %v", err)
}
}
}()
watcher.Add(path)
}
方案二:Viper 自动热更新
import "github.com/spf13/viper"
func InitViper() {
viper.SetConfigFile("config.yaml")
viper.ReadInConfig()
// 开启热更新
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
log.Printf("配置文件变更: %s", e.Name)
// 重新读取配置
var cfg Config
viper.Unmarshal(&cfg)
globalConfig.Store(&cfg)
})
}
// 直接读取(Viper 内部会自动更新)
func GetLogLevel() string {
return viper.GetString("log_level")
}
方案三:etcd 远程配置中心
func WatchEtcdConfig(cli *clientv3.Client, key string) {
// 首次加载
resp, _ := cli.Get(context.Background(), key)
if len(resp.Kvs) > 0 {
applyConfig(resp.Kvs[0].Value)
}
// Watch 变更
watchCh := cli.Watch(context.Background(), key)
for resp := range watchCh {
for _, ev := range resp.Events {
if ev.Type == clientv3.EventTypePut {
log.Printf("配置更新: %s", string(ev.Kv.Value))
applyConfig(ev.Kv.Value)
}
}
}
}
func applyConfig(data []byte) {
var cfg Config
json.Unmarshal(data, &cfg)
globalConfig.Store(&cfg)
}
关键设计要点
线程安全
- 用
atomic.Value存储配置指针,读取无锁 - 整个 Config 结构体一次性替换(不是修改单个字段)
- 不要直接用
sync.RWMutex保护 Config —— 读太频繁,atomic 性能更好
// ✅ 正确:原子替换整个配置
globalConfig.Store(&newConfig)
// ❌ 错误:运行时修改配置字段,有数据竞争
config.LogLevel = "debug"
常见面试问题
Q1: 配置热更新后怎么通知其他模块?
答案:
- 观察者模式:注册回调函数,配置变更时通知
- Channel 通知:
configChangeCh <- newConfig - 各模块轮询
GetConfig()获取最新配置(推荐简单场景)
Q2: 配置更新失败怎么办?
答案:
- 新配置校验通过才替换旧配置
- 失败则保留旧配置,记录错误日志 + 告警
- 配置文件加版本号,可回滚