Pingfan's Blog

golang库——Viper源码分析

字数统计: 2.8k阅读时长: 15 min
2019/04/20 Share

Viper是非常好用的加载配置的golang库。源码分析如下:

分析思路,从一段使用的实际代码来分析一步步流程。

func main() {
 v := viper.New()
 v.SetConfigName("config.development") // 配置文件名
 v.SetConfigType("json") // 配置文件的类型
 v.AddConfigPath("./config/") // 配置文件的路径

 if err := v.ReadInConfig(); err != nil {
  fmt.Println(err)
 }

 fmt.Println(v.Get("port"))
 fmt.Println(v.Get("password"))
 fmt.Println(v.Get("student.age")) // 分析嵌套字段使用“.”即可,非常方便。
 fmt.Println(v.Get("student.hobby"))
 fmt.Println(v.Get("non_exist")) // 不存在这个字段,返回nil
}

新建struct并初始化

Viper结构体定义如下:

type Viper struct {
    // Delimiter that separates a list of keys
    // used to access a nested value in one go
    keyDelim string

    // A set of paths to look for the config file in
    configPaths []string

    // The filesystem to read config from.
    fs afero.Fs

    // A set of remote providers to search for the configuration
    remoteProviders []*defaultRemoteProvider

    // Name of file to look for inside the path
    configName        string
    configFile        string
    configType        string
    configPermissions os.FileMode
    envPrefix         string

    automaticEnvApplied bool
    envKeyReplacer      *strings.Replacer
    allowEmptyEnv       bool

    config         map[string]interface{}
    override       map[string]interface{}
    defaults       map[string]interface{}
    kvstore        map[string]interface{}
    pflags         map[string]FlagValue
    env            map[string]string
    aliases        map[string]string
    typeByDefValue bool

    // Store read properties on the object so that we can write back in order with comments.
    // This will only be used if the configuration read is a properties file.
    properties *properties.Properties

    onConfigChange func(fsnotify.Event)
}

新建一个结构体实例并初始化它:

// New returns an initialized Viper instance.
func New() *Viper {
    v := new(Viper)
    v.keyDelim = "."
    v.configName = "config"   // 默认文件名是config.xxx
    v.configPermissions = os.FileMode(0644)
    v.fs = afero.NewOsFs()  // 用于文件读写。
    v.config = make(map[string]interface{})
    v.override = make(map[string]interface{})
    v.defaults = make(map[string]interface{})
    v.kvstore = make(map[string]interface{})
    v.pflags = make(map[string]FlagValue)
    v.env = make(map[string]string)
    v.aliases = make(map[string]string)
    v.typeByDefValue = false

    return v
}

以后需要实例化的对象都可以使用这种思路来实现。

想起了之前学到的go实现constructor函数,思路也是一模一样,不过可以自定义初始化的参数变量:

package matrix
function NewMatrix(rows, cols int) *matrix {
    m := new(matrix)
    m.rows = rows
    m.cols = cols
    m.elems = make([]float, rows*cols)
    return m
}

设置配置的文件名和文件类型

// SetConfigName sets name for the config file.
// Does not include extension.
func SetConfigName(in string) { v.SetConfigName(in) }
func (v *Viper) SetConfigName(in string) {
    if in != "" {
        v.configName = in
        v.configFile = ""
    }
}

设置viper结构体的configName不需要扩展名

// SetConfigType sets the type of the configuration returned by the
// remote source, e.g. "json".
func SetConfigType(in string) { v.SetConfigType(in) }
func (v *Viper) SetConfigType(in string) {
    if in != "" {
        v.configType = in
    }
}

设置文件的扩展名。

设置配置文件路径

// AddConfigPath adds a path for Viper to search for the config file in.
// Can be called multiple times to define multiple search paths.
func AddConfigPath(in string) { v.AddConfigPath(in) }
func (v *Viper) AddConfigPath(in string) {
    if in != "" {
        absin := absPathify(in)
        jww.INFO.Println("adding", absin, "to paths to search")
        // 如果传入的路径不在configPaths里,则添加到这个数组里面。
        if !stringInSlice(absin, v.configPaths) {
            v.configPaths = append(v.configPaths, absin)
        }
    }
}

首先调用了absPathify这个用户自定义函数,获取完整路径

func absPathify(inPath string) string {
    jww.INFO.Println("Trying to resolve absolute path to", inPath)

    // 如果类似于'$HOME/hello/',则解析成具体的路径,例如'/Users/kpf/hello'
    if strings.HasPrefix(inPath, "$HOME") {
        inPath = userHomeDir() + inPath[5:]
    }

    if strings.HasPrefix(inPath, "$") {
        end := strings.Index(inPath, string(os.PathSeparator))
        inPath = os.Getenv(inPath[1:end]) + inPath[end:]
    }

    // IsAbs,对于'/Users/kpff'这种判断为true,即使它不存在,对于'./kpf'这种判断为false。
    if filepath.IsAbs(inPath) {
        return filepath.Clean(inPath)
    }

    // 如果是相对路径,则返回真实完整路径,例如传入`./config`,则返回的是‘当前运行文件目录+路径’
    // 例如传入'./config',返回的是‘/Users/kpf/Projects/go-src-learn/config’
    p, err := filepath.Abs(inPath)
    if err == nil {
        return filepath.Clean(p)
    }

    jww.ERROR.Println("Couldn't discover absolute path")
    jww.ERROR.Println(err)
    return ""
}

func userHomeDir() string {
    // 如果是在windows上运行。
    if runtime.GOOS == "windows" {
        home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
        if home == "" {
            home = os.Getenv("USERPROFILE")
        }
        return home
    }
    return os.Getenv("HOME")
}

如果传入的路径不在configPaths里,则添加到这个数组里面。

stringInSlice也是封装的函数。。

func stringInSlice(a string, list []string) bool {
    for _, b := range list {
        if b == a {
            return true
        }
    }
    return false
}

总结:这一步将传入的路径写到结构体的configPaths里面。

读取配置信息

这一步是关键,将配置文件的信息读取到内存。

// ReadInConfig will discover and load the configuration file from disk
// and key/value stores, searching in one of the defined paths.
func ReadInConfig() error { return v.ReadInConfig() }
func (v *Viper) ReadInConfig() error {
    jww.INFO.Println("Attempting to read in config file")
    // getConfigFile:获取配置文件完整路径+文件名
    filename, err := v.getConfigFile()
    if err != nil {
        return err
    }
    // 判断是否是支持的文件类型。
    if !stringInSlice(v.getConfigType(), SupportedExts) {
        return UnsupportedConfigError(v.getConfigType())
    }

    jww.DEBUG.Println("Reading file: ", filename)
    // 读取配置文件内容到file(file是个byte数组)
    file, err := afero.ReadFile(v.fs, filename)
    if err != nil {
        return err
    }

    config := make(map[string]interface{})
    // 将配置内容解析到map里。
    err = v.unmarshalReader(bytes.NewReader(file), config)
    if err != nil {
        return err
    }
    // v的config参数 = config map
    v.config = config
    return nil
}

首先是getConfigFile函数:

func (v *Viper) getConfigFile() (string, error) {
    if v.configFile == "" {
        // 返回第一个查找到的配置文件路径。
        cf, err := v.findConfigFile()
        // 如果没找到,返回错误
        if err != nil {
            return "", err
        }
        // 如果找到了,将v.configFile设置成找到的文件路径。
        v.configFile = cf
    }
    return v.configFile, nil
}

在前面配置文件名和文件扩展信息时,有一段代码将configFile设置为空,所以要重新生成configFile.

findConfigFile实现如下:

// Search all configPaths for any config file.
// Returns the first path that exists (and is a config file).
func (v *Viper) findConfigFile() (string, error) {
    jww.INFO.Println("Searching for config in ", v.configPaths)
    // 遍历configPaths
    for _, cp := range v.configPaths {
        file := v.searchInPath(cp)
        // 如果找到了路径下确实存在该文件,则返回完整路径+文件名。
        if file != "" {
            return file, nil
        }
    }
    // 没找到配置文件,抛出错误。
    return "", ConfigFileNotFoundError{v.configName, fmt.Sprintf("%s", v.configPaths)}
}

// 传入一个路径(配置就在这个路径下面)
// 返回
func (v *Viper) searchInPath(in string) (filename string) {
    jww.DEBUG.Println("Searching for config in ", in)
    for _, ext := range SupportedExts {

        jww.DEBUG.Println("Checking for", filepath.Join(in, v.configName+"."+ext))
        // 一个一个的根据configName+ext来匹配。。如果存在该扩展名,则返回true
        // 所以如果相同文件夹下面存在`a.json`和`a.toml`,那么`a.toml`就会被加载成`a.json`...
        if b, _ := exists(v.fs, filepath.Join(in, v.configName+"."+ext)); b {
            jww.DEBUG.Println("Found: ", filepath.Join(in, v.configName+"."+ext))
            return filepath.Join(in, v.configName+"."+ext)
        }
    }

    return ""
}

SupportedExts一开始就声明成一个全局数组:

// SupportedExts are universally supported extensions.
var SupportedExts = []string{"json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl"}

附:自定义错误的实现:

  // ConfigFileNotFoundError denotes failing to find configuration file.
  type ConfigFileNotFoundError struct {
      name, locations string
  }

  // Error returns the formatted configuration error.
  func (fnfe ConfigFileNotFoundError) Error() string {
      return fmt.Sprintf("Config File %q Not Found in %q", fnfe.name, fnfe.locations)
  }


如果成功读取到配置文件,那么将它的所有内容加载到file变量中。

现在最关键的一步是将file内容提取出来,做成key-value的map形式。实现这个功能的函数是unmarshalReader

unmarshalReader

// Unmarshal a Reader into a map.
// Should probably be an unexported function.
func unmarshalReader(in io.Reader, c map[string]interface{}) error {
    return v.unmarshalReader(in, c)
}
// 输入是一个加载好的文件内容和一个map。
// 实现功能:将文件配置解析到map中。
func (v *Viper) unmarshalReader(in io.Reader, c map[string]interface{}) error {
    buf := new(bytes.Buffer)
    buf.ReadFrom(in)

    // 判断配置文件扩展名类型,进行不同处理。
    switch strings.ToLower(v.getConfigType()) {
    case "yaml", "yml":
        if err := yaml.Unmarshal(buf.Bytes(), &c); err != nil {
            return ConfigParseError{err}
        }

    // 如果是json类型,调用json.Unmarshal函数。
    case "json":
        if err := json.Unmarshal(buf.Bytes(), &c); err != nil {
            return ConfigParseError{err}
        }

    case "hcl":
        obj, err := hcl.Parse(string(buf.Bytes()))
        if err != nil {
            return ConfigParseError{err}
        }
        if err = hcl.DecodeObject(&c, obj); err != nil {
            return ConfigParseError{err}
        }

    // 如果是toml类型。调用toml.LoadReader和toMap函数。。
    case "toml":
        tree, err := toml.LoadReader(buf)
        if err != nil {
            return ConfigParseError{err}
        }
        tmap := tree.ToMap()
        for k, v := range tmap {
            c[k] = v
        }

    case "properties", "props", "prop":
        v.properties = properties.NewProperties()
        var err error
        if v.properties, err = properties.Load(buf.Bytes(), properties.UTF8); err != nil {
            return ConfigParseError{err}
        }
        for _, key := range v.properties.Keys() {
            value, _ := v.properties.Get(key)
            // recursively build nested maps
            path := strings.Split(key, ".")
            lastKey := strings.ToLower(path[len(path)-1])
            deepestMap := deepSearch(c, path[0:len(path)-1])
            // set innermost value
            deepestMap[lastKey] = value
        }
    }

    insensitiviseMap(c)
    return nil
}

获取配置信息

之前的所有步骤都是为了实现将配置文件加载到v.config——一个map中。

而v.config是一个私有变量,所以要访问它必须使用getter函数。

viper中使用Get方法来实现:

// 输入一个key,返回interface{}。
// Get can retrieve any value given the key to use.
// Get is case-insensitive for a key.
// Get has the behavior of returning the value associated with the first
// place from where it is set. Viper will check in the following order:
// override, flag, env, config file, key/value store, default
//
// Get returns an interface. For a specific value use one of the Get____ methods.
func Get(key string) interface{} { return v.Get(key) }
func (v *Viper) Get(key string) interface{} {
    lcaseKey := strings.ToLower(key)
    // 调用find方法。
    val := v.find(lcaseKey)
    if val == nil {
        return nil
    }

    if v.typeByDefValue {
        // TODO(bep) this branch isn't covered by a single test.
        valType := val
        path := strings.Split(lcaseKey, v.keyDelim)
        defVal := v.searchMap(v.defaults, path)
        if defVal != nil {
            valType = defVal
        }

        switch valType.(type) {
        case bool:
            return cast.ToBool(val)
        case string:
            return cast.ToString(val)
        case int32, int16, int8, int:
            return cast.ToInt(val)
        case int64:
            return cast.ToInt64(val)
        case float64, float32:
            return cast.ToFloat64(val)
        case time.Time:
            return cast.ToTime(val)
        case time.Duration:
            return cast.ToDuration(val)
        case []string:
            return cast.ToStringSlice(val)
        }
    }

    return val
}

find私有方法代码如下:

// Given a key, find the value.
// Viper will check in the following order:
// flag, env, config file, key/value store, default.
// Viper will check to see if an alias exists first.
// Note: this assumes a lower-cased key given.
func (v *Viper) find(lcaseKey string) interface{} {

    var (
        val    interface{}
        exists bool
        // 根据“.”来分割得到path
        path   = strings.Split(lcaseKey, v.keyDelim)
        // 如果path长度大于1,说明有“.”。是嵌套的key
        nested = len(path) > 1
    )

    // compute the path through the nested maps to the nested value
    if nested && v.isPathShadowedInDeepMap(path, castMapStringToMapInterface(v.aliases)) != "" {
        return nil
    }

    // if the requested key is an alias, then return the proper key
    lcaseKey = v.realKey(lcaseKey)
    path = strings.Split(lcaseKey, v.keyDelim)
    nested = len(path) > 1

    // Set() override first
    val = v.searchMap(v.override, path)
    if val != nil {
        return val
    }
    if nested && v.isPathShadowedInDeepMap(path, v.override) != "" {
        return nil
    }

    // PFlag override next
    flag, exists := v.pflags[lcaseKey]
    if exists && flag.HasChanged() {
        switch flag.ValueType() {
        case "int", "int8", "int16", "int32", "int64":
            return cast.ToInt(flag.ValueString())
        case "bool":
            return cast.ToBool(flag.ValueString())
        case "stringSlice":
            s := strings.TrimPrefix(flag.ValueString(), "[")
            s = strings.TrimSuffix(s, "]")
            res, _ := readAsCSV(s)
            return res
        default:
            return flag.ValueString()
        }
    }
    if nested && v.isPathShadowedInFlatMap(path, v.pflags) != "" {
        return nil
    }

    // Env override next
    if v.automaticEnvApplied {
        // even if it hasn't been registered, if automaticEnv is used,
        // check any Get request
        if val, ok := v.getEnv(v.mergeWithEnvPrefix(lcaseKey)); ok {
            return val
        }
        if nested && v.isPathShadowedInAutoEnv(path) != "" {
            return nil
        }
    }
    envkey, exists := v.env[lcaseKey]
    if exists {
        if val, ok := v.getEnv(envkey); ok {
            return val
        }
    }
    if nested && v.isPathShadowedInFlatMap(path, v.env) != "" {
        return nil
    }

    // Config file next
    val = v.searchMapWithPathPrefixes(v.config, path)
    if val != nil {
        return val
    }
    if nested && v.isPathShadowedInDeepMap(path, v.config) != "" {
        return nil
    }

    // K/V store next
    val = v.searchMap(v.kvstore, path)
    if val != nil {
        return val
    }
    if nested && v.isPathShadowedInDeepMap(path, v.kvstore) != "" {
        return nil
    }

    // Default next
    val = v.searchMap(v.defaults, path)
    if val != nil {
        return val
    }
    if nested && v.isPathShadowedInDeepMap(path, v.defaults) != "" {
        return nil
    }

    // last chance: if no other value is returned and a flag does exist for the value,
    // get the flag's value even if the flag's value has not changed
    if flag, exists := v.pflags[lcaseKey]; exists {
        switch flag.ValueType() {
        case "int", "int8", "int16", "int32", "int64":
            return cast.ToInt(flag.ValueString())
        case "bool":
            return cast.ToBool(flag.ValueString())
        case "stringSlice":
            s := strings.TrimPrefix(flag.ValueString(), "[")
            s = strings.TrimSuffix(s, "]")
            res, _ := readAsCSV(s)
            return res
        default:
            return flag.ValueString()
        }
    }
    // last item, no need to check shadowing

    return nil
}

CATALOG
  1. 1. 新建struct并初始化
  2. 2. 设置配置的文件名和文件类型
  3. 3. 设置配置文件路径
  4. 4. 读取配置信息
  5. 5. unmarshalReader
  6. 6. 获取配置信息