🧑🏫 Go 配置文件解析.
这里使用的是Viper作为Go解析配置文件的公共库.
参考官方文档:www.cnblogs.com/jiujuan/p/1…
🏷️ 一、为什么使用Viper作为Go的配置文件管理.
1.1 Viper 支持的功能:
- 设置默认值.
- 加载䜶格式的配置文件.
- 动态监听配置文件并重新读取配置文件.
- 读取环境变量中的值.
- 读取远程配置系统中的配置值.
- 读取命令行标志作为配置.
- 可以从缓冲区中进行读取配置.
- 设置显示的值.
🏷️ 二、基本使用.
具体代码如下,在我们的其他模块中
// Package config 配置文件模块.
package config
import (
"log"
"go_common/static"
"github.com/spf13/viper"
)
// @author: mazhenxin.
// @create: 2021-09-15 19:26:22
// common模块不应该和配置有关.
// Config 对外提供配置的结构体.
var _config *viper.Viper
// Init 初始配置,采用默认的配置.
// 如果别的模块进行引入的话,那么其加载的配置文件应该在同一个地方.
func Init() {
// 设置配置文件所在的目录.
viper.SetConfigName(static.DefaultConfigFileName)
// 设置配置文件的类型.
viper.SetConfigType(static.DefaultConfigFileType)
// 设置配置文件的目录.
viper.AddConfigPath(static.DefaultConfigPath)
err := viper.ReadInConfig()
if nil != err {
log.Fatalln("init config error: ", err.Error())
}
// 赋值.
_config = viper.GetViper()
}
// GetConfig 对外提供Config配置能力.
func GetConfig() *viper.Viper {
if _config == nil {
// 如果为空,则进行初始化.
Init()
return _config
}
return _config
}
// TODO: 如果还需要其他解析配置文件的模式,可以在通过配置进行添加.
🏷️ 三、Viper 底层原理分析.
简单分析Viper的底层原理,如果在开发环境中使用Viper的话,那么就应该理解其底层原理.
目前来说,一个Viper支持一个配置文件,但是应该可以通过merge来进行合并多个配置文件. 如果要合并的话需要理解合并中如果存在相同Key的问题.
2.1 Viper 代码入口分析:
我们从这里来分析Viper的底层原理.
func Init() {
// 设置配置文件所在的目录.
viper.SetConfigName(static.DefaultConfigFileName)
// 设置配置文件的类型.
viper.SetConfigType(static.DefaultConfigFileType)
// 设置配置文件的目录.
viper.AddConfigPath(static.DefaultConfigPath)
err := viper.ReadInConfig()
if nil != err {
log.Fatalln("init config error: ", err.Error())
}
// 赋值.
_config = viper.GetViper()
}
我们根据Go 中的导包原则,在该包中引入了Viper包,那么Go首先会去加载Viper包中的中的init方法.
// 通过该包我们可以发现,只要引用该包,那么其就会进行初始化一个新的Viper.
func init() {
v = New()
}
2.2 解析前的准备.
- 设置配置文件名字.
// 初始化之后将会通过该方法来设置v中的属性.
func (v *Viper) SetConfigName(in string) {
if in != "" {
v.configName = in
v.configFile = ""
}
}
// 设置配置文件的类型.
func (v *Viper) SetConfigType(in string) {
if in != "" {
v.configType = in
}
}
// 设置配置文件的路径.
func (v *Viper) AddConfigPath(in string) {
if in != "" {
absin := absPathify(in)
jww.INFO.Println("adding", absin, "to paths to search")
// 配置文件可以一次解析解析多个路径下的配置文件.
// v.configPaths是一个切片.
if !stringInSlice(absin, v.configPaths) {
v.configPaths = append(v.configPaths, absin)
}
}
}
在做了上面的解析配置文件的准备之后,我们开始对配置文件进行解析.
2.3 开始解析.
这里我会删掉很多对理解VIper无用的代码.
// 解析配置文件的入口.
err := viper.ReadInConfig()
下面就开始进行配置解析了. 关于每个方法的详细代码在下面也给出了提示.
func (v *Viper) ReadInConfig() error {
// 返回第一个配置文件的名字,这里应该是全路径.
filename, err := v.getConfigFile()
if err != nil {
return err
}
// 判断类型是否Viper支持解析的类型.
if !stringInSlice(v.getConfigType(), SupportedExts) {
return UnsupportedConfigError(v.getConfigType())
}
// 对文件进行解析成字节数组.
file, err := afero.ReadFile(v.fs, filename)
if err != nil {
return err
}
config := make(map[string]interface{})
// 将字节数组转换为map.
// 对于更加底层将字节数组转换为map可以自行去查看.
err = v.unmarshalReader(bytes.NewReader(file), config)
if err != nil {
return err
}
// 保存解析好的配置.
// 这里的v.config是一个map结构.
v.config = config
return nil
}
- getConfigFile方法解析
// Returns the first path that exists (and is a config file).
func (v *Viper) getConfigFile() (string, error) {
if v.configFile == "" {
// 根据添加的配置文件的path去寻找指定的配置文件.
// 这里只会返回第一个出现的配置文件.
// 就是遍历之前添加的ConfigPath.
// 这里会根据v.ConfigfileType进行过滤.
cf, err := v.findConfigFile()
if err != nil {
return "", err
}
v.configFile = cf
}
return v.configFile, nil
}
OK,现在你应该大概了解Viper的运作原理.
🏷️ 四、使用细节.
如何加载多个配置文件?
这里可以使用viper.MergeInConfig来合并多个配置文件到map中,我们看下面的例子.
其读取了两个配置文件,但是经过测试发现,先加载的配置文件中如果和后加载的配置文件的内容有重复,那么后加载的内容将会覆盖前面加载的内容,所以如果要使用全局配置的话,我们应该将全局的配置文件放在后面进行添加.
func Init() {
// 设置配置文件所在的目录.
viper.SetConfigName(static.DefaultConfigFileName)
// 设置配置文件的类型.
viper.SetConfigType(static.DefaultConfigFileType)
// 设置配置文件的目录.
viper.AddConfigPath(static.DefaultConfigPath)
// 动态监听配置文件.
go func() {
viper.WatchConfig()
}()
// TODO: 需要考虑重复问题,就是加载的配置文件向后顺序关系谁会被进行覆盖掉.
err := viper.ReadInConfig()
if nil != err {
log.Fatalln("init config error: ", err.Error())
}
// 再次读取一个新的配置文件之后尽心合并.
viper.SetConfigName("default")
viper.SetConfigType("yaml")
// 我们重点理解MergeInConfig文件.
viper.MergeInConfig()
// 赋值.
_config = viper.GetViper()
}
接下来我们重点理解MergeInConfig方法
func (v *Viper) MergeInConfig() error {
// 获取本次(也就是第二个配置文件)的文件.
filename, err := v.getConfigFile()
if err != nil {
return err
}
if !stringInSlice(v.getConfigType(), SupportedExts) {
return UnsupportedConfigError(v.getConfigType())
}
file, err := afero.ReadFile(v.fs, filename)
if err != nil {
return err
}
// 直接分析该方法.
return v.MergeConfig(bytes.NewReader(file))
}
分析MergeConfig方法.
func (v *Viper) MergeConfig(in io.Reader) error {
cfg := make(map[string]interface{})
if err := v.unmarshalReader(in, cfg); err != nil {
return err
}
// 多个map进行合并.
return v.MergeConfigMap(cfg)
}
我们可以看到其就是多个map进行合并,下面就是先后顺序的问题.
// 第二个参数tgt应该就是要进行保留的map,itgt这里是nil.
func mergeMaps(
src, tgt map[string]interface{}, itgt map[interface{}]interface{}) {
for sk, sv := range src {
tk := keyExists(sk, tgt)
// 在target中不存在该Key.
if tk == "" {
// 将src中在tgt中不存在的key复制过来.
tgt[sk] = sv
if itgt != nil {
itgt[sk] = sv
}
continue
}
//处理存在的key.
tv, ok := tgt[tk]
if !ok {
// 如果不存在.
tgt[sk] = sv
if itgt != nil {
itgt[sk] = sv
}
continue
}
// TODO: 重要.
//srcValue
svType := reflect.TypeOf(sv)
// targetValue.
tvType := reflect.TypeOf(tv)
// 如果两个值的类型不相等,那么使用第一次记载的值.
if tvType != nil && svType != tvType { // Allow for the target to be nil
continue
}
switch ttv := tv.(type) {
// 如果值是map类型就递归进行处理,而最终就是要处理到default中,也就是进行覆盖.
case map[interface{}]interface{}:
jww.TRACE.Printf("merging maps (must convert)")
tsv := sv.(map[interface{}]interface{})
ssv := castToMapStringInterface(tsv)
stv := castToMapStringInterface(ttv)
mergeMaps(ssv, stv, ttv)
case map[string]interface{}:
jww.TRACE.Printf("merging maps")
mergeMaps(sv.(map[string]interface{}), ttv, nil)
default:
// 直接进行值的覆盖.
// 将src中的值覆盖到target中.
// 从这个概念出发我们可以定义全局配置文件以及局部配置文件.
tgt[tk] = sv
if itgt != nil {
itgt[tk] = sv
}
}
}
}
OK!