Go语言使用Viper动态加载配置文件首次重载出现数据竞争问题
Go Viper 动态配置加载数据竞争问题修复方案
问题根因
你遇到的数据竞争来自两个核心问题:
- Viper实例的操作没有全链路加锁
Viper的所有方法(包括初始化时的AddConfigPath、ReadInConfig,启动监听的WatchConfig,以及读取配置的GetString)都会操作内部共享状态,你仅对OnConfigChange回调注册和回调内操作加锁,其他操作没有锁保护,尤其是WatchConfig会启动后台goroutine异步修改Viper内部状态,第一次触发配置变更时,后台goroutine的写操作和你之前的初始化读操作没有同步,就会触发竞争。后续Viper内部状态初始化完成,竞争条件消失因此不再复现,但风险始终存在。 - 全局共享变量
newConfigValue无同步保护
多个goroutine并发读写该全局变量:printUpdatedValueOnly负责写,justAnotherGoroutine、yetAnotherGoroutine负责读,没有任何锁或同步机制,也会触发数据竞争。
修复方案
- 对所有操作Viper实例的逻辑加对应互斥锁,包含初始化、启动监听、回调内读写全链路
- 对全局共享的配置变量加读写锁,或直接通过通道传递配置值避免共享内存
- 将通知通道改为带缓冲,避免发送时阻塞
核心修改代码示例
1. config.go 修改
// setUpConfig 新增锁参数,所有Viper操作加锁保护 func setUpConfig(file string, merge bool, v *viper.Viper, lock *sync.Mutex) { lock.Lock() defer lock.Unlock() v.AddConfigPath("./") v.SetConfigName(file) v.SetConfigType("yml") if merge { err1 := v.MergeInConfig() checkForFatalError("fatal error occurred while reading config file!", err1) } else { err := v.ReadInConfig() checkForFatalError("fatal error occurred while reading config file!", err) } log.Println("Initial config value: ", v.GetString("env")) } func loadConfigDynamically(configChannel chan string, viperLock *sync.Mutex, vipe *viper.Viper) { viperLock.Lock() vipe.OnConfigChange(func(e fsnotify.Event) { viperLock.Lock() log.Println("config file changed", e.Name) environment := vipe.GetString("env") viperLock.Unlock() configChannel <- environment }) // WatchConfig 也要在锁保护下调用,避免和内部异步操作竞争 vipe.WatchConfig() viperLock.Unlock() } func loadMultipleConfigsDynamically() { go func() { time.Sleep(time.Millisecond * 50) vipe2 := viper.New() // 传入锁参数 setUpConfig("config_base", false, vipe2, &viperLock2) loadConfigDynamically(reloadConfig2, &viperLock2, vipe2) time.Sleep(time.Millisecond * 50) vipe1 := viper.New() // 传入锁参数 setUpConfig("config", false, vipe1, &viperLock1) loadConfigDynamically(reloadConfig, &viperLock1, vipe1) }() }
2. main.go 修改
var ( reloadConfigNow = make(chan bool, 1) // 新增1个缓冲,避免发送阻塞 reloadConfigAgain = make(chan bool, 1) newConfigValue string configLock sync.RWMutex // 新增配置读写锁 ) func printUpdatedValueOnly() { for { select { case updatedValue := <-reloadConfig: // 写全局变量加写锁 configLock.Lock() newConfigValue = updatedValue configLock.Unlock() log.Println("dynamically loaded config value: ", updatedValue) reloadConfigNow <-true reloadConfigAgain <-true case updatedValue1 := <-reloadConfig2: // 写全局变量加写锁 configLock.Lock() newConfigValue = updatedValue1 configLock.Unlock() log.Println("dynamically loaded config value: ", updatedValue1) reloadConfigNow <-true reloadConfigAgain <-true default: } } } func justAnotherGoroutine(){ existingConfigValue := "" for { select { case <-reloadConfigNow: // 读全局变量加读锁 configLock.RLock() existingConfigValue = newConfigValue configLock.RUnlock() log.Println("justAnotherGoroutine: ", existingConfigValue) default: } } } func yetAnotherGoroutine() { existingConfigValue := "" for { select { case <-reloadConfigAgain: // 读全局变量加读锁 configLock.RLock() existingConfigValue = newConfigValue configLock.RUnlock() log.Println("yetAnotherGoroutine: ", existingConfigValue) default: } } }
修改完成后再执行go run -race reload_config测试,即可消除数据竞争问题。
内容的提问来源于stack exchange,提问作者HSAR
相关产品推荐
相关产品推荐

