阅读 256

Go 中的"自动装配"。

🧑‍🏫 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!

文章分类
后端
文章标签