Viper 是 Go 应用程序现代化的、完整的解决方案,能够处理不同格式的配置文件,让我们在构建现代应用程序时,不必担心配置文件格式。Viper 也能够满足我们对应用配置的各种需求。

Viper 可以从不同的位置读取配置,不同位置的配置具有不同的优先级,高优先级的配置会覆盖低优先级相同的配置,按优先级从高到低排列如下:

  1. 通过 viper.Set 函数显示设置的配置
  2. 命令行参数
  3. 环境变量
  4. 配置文件
  5. Key/Value 存储
  6. 默认值

这里需要注意,Viper 配置键不区分大小写。

读入配置

读入配置,就是将配置读入到 Viper 中,有如下读入方式:

  • 设置默认的配置文件名。
  • 读取配置文件。
  • 监听和重新读取配置文件。
  • io.Reader 读取配置。
  • 从环境变量读取。
  • 从命令行标志读取。
  • 从远程 Key/Value 存储读取。

设置默认

一个好的配置系统应该支持默认值。Viper 支持对 key 设置默认值,当没有通过配置文件、环境变量、远程配置或命令行标志设置 key 时,设置默认值通常是很有用的,可以让程序在没有明确指定配置时也能够正常运行。例如:

viper.SetDefault("ContentDir", "content")
viper.SetDefault("LayoutDir", "layouts")
viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "categories"})

读取配置文件

Viper 可以读取配置文件来解析配置,支持 JSON、TOML、YAML、YML、Properties、Props、Prop、HCL、Dotenv、Env 格式的配置文件。Viper 支持搜索多个路径,并且默认不配置任何搜索路径,将默认决策留给应用程序。

以下是如何使用 Viper 搜索和读取配置文件的示例:

package main

import (
  "fmt"

  "github.com/spf13/pflag"
  "github.com/spf13/viper"
)

var (
  cfg  = pflag.StringP("config", "c", "", "Configuration file.")
  help = pflag.BoolP("help", "h", false, "Show this help message.")
)

func main() {
  pflag.Parse()
  if *help {
    pflag.Usage()
    return
  }

  // 从配置文件中读取配置
  if *cfg != "" {
    viper.SetConfigFile(*cfg)   // 指定配置文件名
    viper.SetConfigType("yaml") // 如果配置文件名中没有文件扩展名,则需要指定配置文件的格式,告诉viper以何种格式解析文件
  } else {
    viper.AddConfigPath(".")          // 把当前目录加入到配置文件的搜索路径中
    viper.AddConfigPath("$HOME/.iam") // 配置文件搜索路径,可以设置多个配置文件搜索路径
    viper.SetConfigName("config")     // 配置文件名称(没有文件扩展名)
  }

  if err := viper.ReadInConfig(); err != nil { // 读取配置文件。如果指定了配置文件名,则使用指定的配置文件,否则在注册的搜索路径中搜索
    panic(fmt.Errorf("Fatal error config file: %s \n", err))
  }

  fmt.Printf("Used configuration file is: %s\n", viper.ConfigFileUsed())
}

Viper 支持设置多个配置文件搜索路径,需要注意添加搜索路径的顺序,Viper 会根据添加的路径顺序搜索配置文件,如果找到则停止搜索。如果调用 SetConfigFile 直接指定了配置文件名,并且配置文件名没有文件扩展名时,需要显式指定配置文件的格式,以使 Viper 能够正确解析配置文件。

如果通过搜索的方式查找配置文件,则需要注意,SetConfigName 设置的配置文件名是不带扩展名的,在搜索时 Viper 会在文件名之后追加文件扩展名,并尝试搜索所有支持的扩展类型。

监听和重新读取

**Viper 支持在运行时让应用程序实时读取配置文件,也就是热加载配置。**可以通过 WatchConfig 函数热加载配置。在调用 WatchConfig 函数之前,需要确保已经添加了配置文件的搜索路径。另外,还可以为 Viper 提供一个回调函数,以便在每次发生更改时运行。这里我也给你个示例:

viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
   // 配置文件发生变更之后会调用的回调函数
  fmt.Println("Config file changed:", e.Name)
})

不建议在实际开发中使用热加载功能,因为即使配置热加载了,程序中的代码也不一定会热加载。例如:修改了服务监听端口,但是服务没有重启,这时候服务还是监听在老的端口上,会造成不一致。

我们可以通过 viper.Set() 函数来显式设置配置:

viper.Set("user.username", "colin")

Viper 还支持环境变量,通过如下 5 个函数来支持环境变量:

  • AutomaticEnv()
  • BindEnv(input …string) error
  • SetEnvPrefix(in string)
  • SetEnvKeyReplacer(r *strings.Replacer)
  • AllowEmptyEnv(allowEmptyEnv bool)

这里要注意:Viper 读取环境变量是区分大小写的。Viper 提供了一种机制来确保 Env 变量是唯一的。通过使用 SetEnvPrefix,可以告诉 Viper 在读取环境变量时使用前缀。BindEnv 和 AutomaticEnv 都将使用此前缀。比如,我们设置了 viper.SetEnvPrefix(“VIPER”),当使用 viper.Get(“apiversion”) 时,实际读取的环境变量是VIPER_APIVERSION

BindEnv 需要一个或两个参数。第一个参数是键名,第二个是环境变量的名称,环境变量的名称区分大小写。如果未提供 Env 变量名,则 Viper 将假定 Env 变量名为:环境变量前缀_键名全大写。例如:前缀为 VIPER,key 为 username,则 Env 变量名为VIPER_USERNAME。当显示提供 Env 变量名(第二个参数)时,它不会自动添加前缀。例如,如果第二个参数是 ID,Viper 将查找环境变量 ID。

还有一个魔法函数 SetEnvKeyReplacer,SetEnvKeyReplacer 允许你使用 strings.Replacer 对象来重写 Env 键。如果你想在 Get() 调用中使用-或者.,但希望你的环境变量使用*分隔符,可以通过 SetEnvKeyReplacer 来实现。比如,我们设置了环境变量USER_SECRET_KEY=bVix2WBv0VPfrDrvlLWrhEdzjLpPCNYb,但我们想用viper.Get("user.secret-key"),那我们就调用函数:*