🧑🏫 Go configuration file parsing.
Viper is used here as the common library for Go parsing configuration files.
Refer to the official documentation: www.cnblogs.com/jiujuan/p/1…
🏷️ I. Why use Viper as Go configuration file management?
1.1 Viper supported functions:
- Set the default values.
- Load the configuration file in 䜶 format.
- Dynamically listen to and re-read the configuration file.
- Read values in environment variables.
- Reads configuration values in the remote configuration system.
- Read command line flags as configuration.
- Read configuration can be done from the buffer.
- Sets the value displayed.
🏷️ 2. Basic use
The specific code is as follows, in our other modules
// Package config Config module.
package config
import (
"log"
"go_common/static"
"github.com/spf13/viper"
)
// @author: mazhenxin.
// @create: 2021-09-15 19:26:22
// The common module should not be configuration dependent.
// Config Specifies the external configuration structure.
var _config *viper.Viper
// Init Initial configuration, using the default configuration.
// If other modules import, their loaded configuration files should be in the same place.
func Init(a) {
// Set the directory where the configuration file resides.
viper.SetConfigName(static.DefaultConfigFileName)
// Set the type of the configuration file.
viper.SetConfigType(static.DefaultConfigFileType)
// Set the directory for the configuration file.
viper.AddConfigPath(static.DefaultConfigPath)
err := viper.ReadInConfig()
if nil! = err { log.Fatalln("init config error: ", err.Error())
}
/ / assignment.
_config = viper.GetViper()
}
// GetConfig provides the Config configuration capability externally.
func GetConfig(a) *viper.Viper {
if _config == nil {
// If empty, initialize.
Init()
return _config
}
return _config
}
// TODO:If you need other modes for parsing configuration files, you can add them through configuration.
Copy the code
🏷️ iii. Analysis of the underlying principle of Viper
A brief analysis of the underlying principles of Viper, which you should understand if you are using Viper in a development environment.
Currently, one Viper supports one configuration file, but it should be possible to merge multiple configuration files using merge. If you want to merge, you need to understand if the same Key exists in the merge.
2.1 Viper code Entry Analysis:
This is where we analyze the underlying principles of Viper.
func Init(a) {
// Set the directory where the configuration file resides.
viper.SetConfigName(static.DefaultConfigFileName)
// Set the type of the configuration file.
viper.SetConfigType(static.DefaultConfigFileType)
// Set the directory for the configuration file.
viper.AddConfigPath(static.DefaultConfigPath)
err := viper.ReadInConfig()
if nil! = err { log.Fatalln("init config error: ", err.Error())
}
/ / assignment.
_config = viper.GetViper()
}
Copy the code
We introduced the Viper package in the package according to the package guide principle in Go, so Go will first load the init method in the Viper package.
// We can see from this package that whenever this package is referenced, it initializes a new Viper.
func init(a) {
v = New()
}
Copy the code
2.2 Preparations before Parsing.
- Set the profile name.
// This method will be used to set the properties in v after initialization.
func (v *Viper) SetConfigName(in string) {
ifin ! ="" {
v.configName = in
v.configFile = ""}}// Set the type of the configuration file.
func (v *Viper) SetConfigType(in string) {
ifin ! ="" {
v.configType = in
}
}
// Set the path to the configuration file.
func (v *Viper) AddConfigPath(in string) {
ifin ! ="" {
absin := absPathify(in)
jww.INFO.Println("adding", absin, "to paths to search")
// Configuration file You can parse configuration files in multiple paths at a time.
// v.configPaths is a slice.
if! stringInSlice(absin, v.configPaths) { v.configPaths =append(v.configPaths, absin)
}
}
}
Copy the code
After doing the above preparation for parsing the configuration file, we begin parsing the configuration file.
2.3 Start Parsing.
I’m going to cut out a lot of code here that’s useless for understanding VIper.
// An entry to parse the configuration file.
err := viper.ReadInConfig()
Copy the code
Now it’s time for configuration parsing. Detailed code for each method is also indicated below.
func (v *Viper) ReadInConfig(a) error {
// Return the name of the first configuration file, which should be the full path.
filename, err := v.getConfigFile()
iferr ! =nil {
return err
}
// Determine whether Viper supports parsed types.
if! stringInSlice(v.getConfigType(), SupportedExts) {return UnsupportedConfigError(v.getConfigType())
}
// Parse the file into a byte array.
file, err := afero.ReadFile(v.fs, filename)
iferr ! =nil {
return err
}
config := make(map[string]interface{})
// Convert the byte array to a map.
// For more low-level conversion of byte arrays to maps, see for yourself.
err = v.unmarshalReader(bytes.NewReader(file), config)
iferr ! =nil {
return err
}
// Save the parsed configuration.
// here v.config is a map structure.
v.config = config
return nil
}
Copy the code
- GetConfigFile method resolution
// Returns the first path that exists (and is a config file).
func (v *Viper) getConfigFile(a) (string, error) {
if v.configFile == "" {
// Find the specified configuration file based on the path of the added configuration file.
// Only the first configuration file is returned.
// To iterate through the ConfigPath added earlier.
// this will be filtered according to v.conFigFileType.
cf, err := v.findConfigFile()
iferr ! =nil {
return "", err
}
v.configFile = cf
}
return v.configFile, nil
}
Copy the code
OK, now you should have some idea how Viper works.
🏷️ 4. Use details.
How do I load multiple configuration files?
Here you can use viper.mergeinConfig to merge multiple configuration files into a map, as shown in the following example.
Its read two configuration files, but after testing found that after loading the configuration file if and the content is loading the configuration file, then after loading the content of the content will cover the front loading, so if you want to use global configuration, we should add on global configuration file on the back.
func Init(a) {
// Set the directory where the configuration file resides.
viper.SetConfigName(static.DefaultConfigFileName)
// Set the type of the configuration file.
viper.SetConfigType(static.DefaultConfigFileType)
// Set the directory for the configuration file.
viper.AddConfigPath(static.DefaultConfigPath)
// Dynamically listen on configuration files.
go func(a) {
viper.WatchConfig()
}()
// TODO:Consider duplication, which is the backward order of loaded configuration files in relation to who gets overwritten.
err := viper.ReadInConfig()
if nil! = err { log.Fatalln("init config error: ", err.Error())
}
// Read a new configuration file again and merge it.
viper.SetConfigName("default")
viper.SetConfigType("yaml")
// We focus on the MergeInConfig file.
viper.MergeInConfig()
/ / assignment.
_config = viper.GetViper()
}
Copy the code
Next, let’s focus on the MergeInConfig method
func (v *Viper) MergeInConfig(a) error {
// Get the file for this (that is, the second configuration file).
filename, err := v.getConfigFile()
iferr ! =nil {
return err
}
if! stringInSlice(v.getConfigType(), SupportedExts) {return UnsupportedConfigError(v.getConfigType())
}
file, err := afero.ReadFile(v.fs, filename)
iferr ! =nil {
return err
}
// Analyze the method directly.
return v.MergeConfig(bytes.NewReader(file))
}
Copy the code
Analyze the MergeConfig method.
func (v *Viper) MergeConfig(in io.Reader) error {
cfg := make(map[string]interface{})
iferr := v.unmarshalReader(in, cfg); err ! =nil {
return err
}
// Merge multiple maps.
return v.MergeConfigMap(cfg)
}
Copy the code
We can see that it is a combination of multiple maps, and the following is the order of precedence.
// The second parameter TGT should be the map to be reserved,itgt is nil.
func mergeMaps(
src, tgt map[string]interface{}, itgt map[interface{}]interface{}) {
for sk, sv := range src {
tk := keyExists(sk, tgt)
// The Key does not exist in target.
if tk == "" {
// Copy SRC keys that do not exist in TGT.
tgt[sk] = sv
ifitgt ! =nil {
itgt[sk] = sv
}
continue
}
// Handle existing keys.
tv, ok := tgt[tk]
if! ok {// If it does not exist.
tgt[sk] = sv
ifitgt ! =nil {
itgt[sk] = sv
}
continue
}
// TODO:Important.
//srcValue
svType := reflect.TypeOf(sv)
// targetValue.
tvType := reflect.TypeOf(tv)
// If the two values are of different types, the first recorded value is used.
iftvType ! =nil&& svType ! = tvType {// Allow for the target to be nil
continue
}
switch ttv := tv.(type) {
// If the value is of map type, it is processed recursively, which is finally processed into default, that is, overwritten.
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:
// Override the value directly.
// Overwrite the value from SRC to target.
// From this concept we can define global configuration files and local configuration files.
tgt[tk] = sv
ifitgt ! =nil {
itgt[tk] = sv
}
}
}
}
Copy the code
OK!