type
status
date
slug
summary
tags
category
icon
password
第一个问题:SetConfigType()真的有用吗?
问题引入
当你使用如下方式读取配置时,viper会从
./conf
目录下查找任何以config
为文件名的配置文件,如果同时存在./conf/config.json
和./conf/config.yaml
两个配置文件的话,viper
会从哪个配置文件加载配置呢?复现
下面的 demo 代码模拟这种情况
发现输出的是
Name: Serendipity_json, Age: 20
之后我们尝试加上
viper.SetConfigType("yaml")
这一行代码,运行发现结果并没有因为加上这一行而改变。运行结果

分析
首先来看一下为什么在没有指明文件类型的时候会读取到
json
文件。viper
会按照文件系统的顺序查找文件,在你设置的路径下依次尝试加载config.json
、config.yaml
、config.toml
等文件格式。
- 默认情况下,
.json
文件会被优先加载,如果同时存在config.json
和config.yaml
,viper
会加载config.json
文件。
明白了这一点后我们再来看为什么加上
SetConfigType("yaml")
后结果依旧不变。我们来看一下 Viper 中
viper.ReadInConfig()
的源码
可以看到,只有在
stringInSlice()
中用到过 v.getConfigType()
,也就是获取文件的种类,在读取文件的时候并没有做文件名称和种类的拼接,导致这个 SetConfigType()
并没有起到实质性确定文件类型的作用。解决方案
第二个问题:热更新配置一致性问题
问题引入
假设我们当前服务中有一条流水线操作,需要分别调用三个接口A、B、C才能完成对应的功能,其中配置文件存储了调用的接口名称。
现在我们需要将流水线要执行的流程从ABC换成DEF,在替换过程中就可能出现热更新冲突的问题。
复现
- 业务协程开始从配置文件读取接口A的配置,读取完成后调用接口A。
- 在接口A调用尚未返回时,WatchConfig监听到配置文件变化,触发热更新OnConfigChange,配置文件变化如下:
- 此时协程继续按照流水线流程读取配置,读取到下一个要执行的接口是E,这里就破坏了流程的完整性,与我们理想状态下的ABC或者DEF的执行流程不一致,可能导致无法预估的错误出现。
以下demo程序模拟了这种情况
运行结果:

分析
我们先来看一下
WatchConfig()
的源码
简单来说就是通过
WatchConfig()
先初始化一个 viper
对象,完成后开始进行文件变更事件的监听,OnConfigChange()
负责将用户定义的回调逻辑赋值给 viper
对象的 onConfigChange()
,此时文件如果发生对应的变更,则会触发对应的回调逻辑。因此我们可以得出产生热更新冲突点原因:
- 并发读写未同步
viper
默认未对内存中的配置数据加锁,当多个goroutine
同时读写配置时,会引发竞争。
- 配置存储非原子性配置文件写入中途被读取(如文件未完全写入),会导致读取到损坏或不完整的数据。
解决方案
方案一:加读写锁
实现思路
- 全局配置对象的建立:为了便于管理多个系统的共享配置资源,我们将所有系统的相关配置集中存储在一个全局的配置对象中。通过这种设计,可以避免因同一配置对象被不同部分重复读取导致的操作异常。
- 线程安全机制的实现:在对全局配置进行更新时,直接修改原对象可能会引发多线程竞争和不一致性问题。为此,我们采用“读写锁“的方式,确保对配置对象的所有操作均需通过锁进行互斥处理。这种机制能够在任何时间点确保只有一个线程对配置对象进行修改。
- 防止更新阻塞的技术:为了避免因配置更新导致的线程阻塞问题以及确保数据一致性,我们在全局配置发生更新后采取以下措施:首先,在相关组件中触发回调机制,以通知其获取最新的配置信息;其次,启动一个协程来执行更新操作,这种方式可以有效避免因单一操作引发的资源阻塞,并确保其他协程线程能够正常读取和处理数据。
源码
运行结果:

方案二:原子操作
实现思路
- 全局配置对象:通过全局配置对象的Store`方法,在初始化及更新阶段获取最新的配置信息。
- 复制读取:Load方法以读取复制一份当前的状态。这种方式确保了每次读取的数据都是最新版本的副本,避免数据不一致的风险。
- 原子性操作:由于这些操作采用的是原子性机制,避免了显式的锁管理,因此在性能上具有显著优势。相比传统的加锁方式,这种做法能够有效减少资源竞争和同步开销,从而提升了系统的整体效率。
源码
运行结果:

换成多个协程不同时运看看效果

第三个问题:AddConfigPath()路径究竟怎写?
问题引入
正常启动项目,main 函数,都使用同位置可以正常读取配置,但是在单元测试调用初始化函数时,出现了无法找到配置文件的问题。
复现
分析
列出当前关键目录
尝试在测试测试函数中输出当前路径,发现输出结果为
Muxi-Micro-Layout/conf/conf
,这显然不符合我们的预期。主要原因是对
./
有误解,我一直以为 ./
的意思是当前项目的根目录,在 go 中 ./
是基于执行命令的目录的,也就是说在不同的目录下调用 Init()
,./
所代表的意义不同。解决方案
因为是直接获取的
config.go
文件的绝对目录,所以无论在哪里调用配置初始化函数,都不会出现找不到文件的问题了参考链接:
官网:https://github.com/spf13/viper
- Author:Serendipity
- URL:https://serendipity565.netlify.app//article/viper%E7%9A%84%E2%80%9C%E9%99%B7%E9%98%B1%E2%80%9D
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!