安装

  1. go get github.com/spf13/viper

Viper是什么?

viper是一个完整的配置解决方案的Go应用程序,包括12个因素的应用程序。它被设计为在应用程序中工作,并且可以处理所有类型的配置需求和格式。它支持:

  • 设置默认值
  • 读取JSON, TOML, YAML, HCL, envfile和Java属性配置文件
  • 实时查看和重新读取配置文件(可选)
  • 从环境变量中读取
  • 从远程配置系统(etcd或领事)读取,并观察变化
  • 从命令行标志读取
  • 读取缓冲区
  • 设置明确的值

可以将Viper看作您所有应用程序配置需要的注册中心。

为什么选择Viper?

在构建一个现代应用程序时,您不需要担心配置文件格式;你要专注于开发出色的软件。viper会来帮忙的。

viper为您做以下工作:

  1. 查找、加载和解编JSON、TOML、YAML、HCL、INI、envfile或Java属性格式的配置文件。
  2. 提供一种机制来为不同的配置选项设置默认值。
  3. 提供一种机制,为通过命令行标志指定的选项设置覆盖值。
  4. 提供别名系统,以便在不破坏现有代码的情况下轻松重命名参数。
  5. 当用户提供了与默认配置相同的命令行或配置文件时,很容易区分。

viper使用以下优先顺序。每个项目优先于它下面的项目:

  • explicit call to Set
  • flag
  • env
  • config
  • key/value store
  • default

重要:viper配置键不区分大小写。目前正在讨论是否将其作为可选项。
**

赋值

建立默认值

一个好的配置系统将支持默认值。键不需要默认值,但是在没有通过配置文件、环境变量、远程配置或标志设置键的情况下,它很有用。

例子:

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

读取配置文件

Viper需要最小的配置,所以它知道在哪里查找配置文件。Viper支持JSON, TOML, YAML, HCL, INI, envfile和Java属性文件。Viper可以搜索多个路径,但目前单个Viper实例只支持单个配置文件。Viper不默认任何配置搜索路径,将默认决策留给应用程序。

下面是如何使用Viper搜索和读取配置文件的示例。不需要任何特定的路径,但是在需要配置文件的地方至少应该提供一个路径。

  1. viper.SetConfigName("config") // name of config file (without extension)
  2. viper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name
  3. viper.AddConfigPath("/etc/appname/") // path to look for the config file in
  4. viper.AddConfigPath("$HOME/.appname") // call multiple times to add many search paths
  5. viper.AddConfigPath(".") // optionally look for config in the working directory
  6. err := viper.ReadInConfig() // Find and read the config file
  7. if err != nil { // Handle errors reading the config file
  8. panic(fmt.Errorf("Fatal error config file: %s \n", err))
  9. }

你可以像这样处理没有配置文件的情况:

  1. if err := viper.ReadInConfig(); err != nil {
  2. if _, ok := err.(viper.ConfigFileNotFoundError); ok {
  3. // Config file not found; ignore error if desired
  4. } else {
  5. // Config file was found but another error was produced
  6. }
  7. }
  8. // Config file found and successfully parsed

注意[自1.6]:你也可以有一个没有扩展名的文件,并指定程序格式。对于那些位于用户主服务器中没有.bashrc之类扩展名的配置文件

写入配置文件

从配置文件中读取是有用的,但有时您希望存储在运行时所做的所有修改。为此,有一堆命令可用,每个都有自己的用途:

  • WriteConfig——如果存在,则将当前的viper配置写入预定义的路径。如果没有预定义的路径,则出错。将覆盖当前配置文件(如果存在)。
  • SafeWriteConfig—将当前的viper配置写入预定义的路径。如果没有预定义的路径,则出错。不会覆盖当前配置文件(如果存在)。
  • WriteConfigAs——将当前的viper配置写入给定的文件路径。将覆盖给定文件(如果存在)。
  • SafeWriteConfigAs——将当前的viper配置写入给定的文件路径。不会覆盖给定文件(如果存在)。

作为一个经验法则,所有标记为safe的文件都不会覆盖任何文件,如果不存在就创建,而默认行为是创建或截断。

一个小示例部分:

  1. viper.WriteConfig() // writes current config to predefined path set by 'viper.AddConfigPath()' and 'viper.SetConfigName'
  2. viper.SafeWriteConfig()
  3. viper.WriteConfigAs("/path/to/my/.config")
  4. viper.SafeWriteConfigAs("/path/to/my/.config") // will error since it has already been written
  5. viper.SafeWriteConfigAs("/path/to/my/.other_config")

查看和重读配置文件

Viper支持让您的应用程序在运行时实时读取配置文件。

需要重新启动服务器以使配置生效的日子已经一去不复返了,viper驱动的应用程序可以在运行时读取配置文件的更新,不会错过任何一个节奏。

只需告诉viper实例watchConfig。还可以为Viper提供一个函数,以便在每次发生更改时运行。

确保在调用WatchConfig()之前添加了所有的配置路径。

  1. viper.WatchConfig()
  2. viper.OnConfigChange(func(e fsnotify.Event) {
  3. fmt.Println("Config file changed:", e.Name)
  4. })

从io.Reader中读取配置

Viper预先定义了许多配置源,比如文件、环境变量、标志和远程K/V存储,但是您没有绑定到它们。您还可以实现自己所需的配置源文件,并将其提供给viper。

  1. viper.SetConfigType("yaml") // or viper.SetConfigType("YAML")
  2. // any approach to require this configuration into your program.
  3. var yamlExample = []byte(`
  4. Hacker: true
  5. name: steve
  6. hobbies:
  7. - skateboarding
  8. - snowboarding
  9. - go
  10. clothing:
  11. jacket: leather
  12. trousers: denim
  13. age: 35
  14. eyes : brown
  15. beard: true
  16. `)
  17. viper.ReadConfig(bytes.NewBuffer(yamlExample))
  18. viper.Get("name") // this would be "steve"

覆盖设置

这些可能来自命令行标志,也可能来自您自己的应用程序逻辑。

  1. viper.Set("Verbose", true)
  2. viper.Set("LogFile", LogFile)

注册和使用别名

别名允许多个键引用一个值

  1. viper.RegisterAlias("loud", "Verbose")
  2. viper.Set("verbose", true) // same result as next line
  3. viper.Set("loud", true) // same result as prior line
  4. viper.GetBool("loud") // true
  5. viper.GetBool("verbose") // true

使用环境变量

Viper完全支持环境变量。这使得12个因素的应用程序开箱。有五种方法可以帮助您与ENV合作:

  • AutomaticEnv()
  • BindEnv(string...) : error
  • SetEnvPrefix(string)
  • SetEnvKeyReplacer(string...) *strings.Replacer
  • AllowEmptyEnv(bool)

在处理ENV变量时,务必认识到Viper将ENV变量视为大小写敏感变量。

Viper提供了一种机制来确保环境变量是唯一的。通过使用
SetEnvPrefix,您可以告诉Viper在读取环境变量时使用前缀。BindEnvAutomaticEnv**都将使用这个前缀。

BindEnv接受一个或多个参数。第一个参数是键名,其余是要绑定到该键的环境变量的名称。如果提供了多个,它们将按照指定的顺序优先。环境变量的名称区分大小写。如果没有提供ENV变量名,那么Viper将自动假设ENV变量匹配以下格式:prefix + “_” +全大写的键名。当显式地提供ENV变量名(第二个参数)时,它不会自动添加前缀。例如,如果第二个参数是“id”,Viper将查找ENV变量“id”。

在使用ENV变量时需要认识到的一件重要事情是,每次访问该值时都会读取它。当BindEnv被调用时,Viper不会固定这个值。

AutomaticEnv是一个强大的助手,特别是当与SetEnvPrefix结合使用时。当被调用时,Viper将在任何时候检查环境变量。发出Get请求。它将应用以下规则。它将检查一个环境变量,该环境变量的名称是否与大写键匹配,如果设置了EnvPrefix前缀。

SetEnvKeyReplacer允许使用字符串。对象来重写一定范围内的Env键。如果您想在Get()调用中使用-或其他东西,但又希望环境变量使用_分隔符,那么这是很有用的。使用它的示例可以在viper_test.go中找到。

或者,您可以使用EnvKeyReplacer与NewWithOptions工厂函数。与SetEnvKeyReplacer不同,它接受StringReplacer接口,允许您编写定制的字符串替换逻辑。

默认情况下,空环境变量被认为是未设置的,并将返回到下一个配置源。要将空的环境变量作为set处理,请使用AllowEmptyEnv方法。

Env 例子

  1. SetEnvPrefix("spf") // will be uppercased automatically
  2. BindEnv("id")
  3. os.Setenv("SPF_ID", "13") // typically done outside of the app
  4. id := Get("id") // 13

使用Flags

viper有绑定flags的能力。具体来说,Viper支持Cobra中使用的Pflags。

与BindEnv一样,该值不是在调用绑定方法时设置的,而是在访问该方法时设置的。这意味着您可以尽可能早地进行绑定,即使是在init()函数中。

对于单个flags,BindPFlag()方法提供了此功能。
例子:

  1. serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
  2. viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))

您也可以绑定一个现有的pflag(pflag.FlagSet):
例子:

  1. pflag.Int("flagname", 1234, "help message for flagname")
  2. pflag.Parse()
  3. viper.BindPFlags(pflag.CommandLine)
  4. i := viper.GetInt("flagname") // retrieve values from viper instead of pflag

在Viper中使用pflag并不排除使用标准库中标记包的其他包的使用。pflag包可以通过导入这些标志来处理为标志包定义的标志。这是通过调用pflag包提供的一个方便函数AddGoFlagSet()来完成的。
例子:

  1. package main
  2. import (
  3. "flag"
  4. "github.com/spf13/pflag"
  5. )
  6. func main() {
  7. // using standard library "flag" package
  8. flag.Int("flagname", 1234, "help message for flagname")
  9. pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
  10. pflag.Parse()
  11. viper.BindPFlags(pflag.CommandLine)
  12. i := viper.GetInt("flagname") // retrieve value from viper
  13. ...
  14. }

Flag 接口

如果不使用Pflags, Viper提供了两个Go接口来绑定其他标记系统。
FlagValue表示单个标志。这是一个非常简单的例子,如何实现这个接口:

  1. type myFlag struct {}
  2. func (f myFlag) HasChanged() bool { return false }
  3. func (f myFlag) Name() string { return "my-flag-name" }
  4. func (f myFlag) ValueString() string { return "my-flag-value" }
  5. func (f myFlag) ValueType() string { return "string" }

一旦你的flag实现了这个接口,你可以简单地告诉Viper来绑定它:

  1. viper.BindFlagValue("my-flag-name", myFlag{})

FlagValueSet表示一组标志。这是一个非常简单的例子,如何实现这个接口:

  1. type myFlagSet struct {
  2. flags []myFlag
  3. }
  4. func (f myFlagSet) VisitAll(fn func(FlagValue)) {
  5. for _, flag := range flags {
  6. fn(flag)
  7. }
  8. }

一旦你的flag设置实现了这个接口,你可以简单地告诉Viper绑定它:

  1. fSet := myFlagSet{
  2. flags: []myFlag{myFlag{}, myFlag{}},
  3. }
  4. viper.BindFlagValues("my-flags", fSet)

远程key/value存储支持

要在Viper中启用远程支持,请空白导入Viper /remote包:

  1. import _ "github.com/spf13/viper/remote"

Viper将读取从键/值存储(如etcd或领事)中的路径中检索到的配置字符串(如JSON、TOML、YAML、HCL或envfile)。这些值优先于默认值,但会被从磁盘、标志或环境变量检索的配置值覆盖。

Viper使用crypt从K/V存储库检索配置,这意味着您可以加密存储配置值,并让它们自动解密,如果您有正确的gpg密匙环。加密是可选的。

可以将远程配置与本地配置结合使用,也可以独立于本地配置使用。

crypt有一个命令行助手,您可以使用它将配置放到K/V存储中。crypt默认为etcd,地址是http://127.0.0.1:4001

  1. $ go get github.com/bketelsen/crypt/bin/crypt
  2. $ crypt set -plaintext /config/hugo.json /Users/hugo/settings/config.json

确认您的值已设置:

  1. $ crypt get -plaintext /config/hugo.json

有关如何设置加密值或如何使用领事的示例,请参阅crypt文档。

远程密钥/值存储示例-未加密

etcd

  1. viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001","/config/hugo.json")
  2. viper.SetConfigType("json") // because there is no file extension in a stream of bytes, supported extensions are "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv"
  3. err := viper.ReadRemoteConfig()

Consul

您需要用包含所需配置的JSON值设置一个键来执行键/值存储。例如,创建一个具有值的代理键/值存储键MY_CONSUL_KEY:

  1. {
  2. "port": 8080,
  3. "hostname": "myhostname.com"
  4. }
  1. viper.AddRemoteProvider("consul", "localhost:8500", "MY_CONSUL_KEY")
  2. viper.SetConfigType("json") // Need to explicitly set this to json
  3. err := viper.ReadRemoteConfig()
  4. fmt.Println(viper.Get("port")) // 8080
  5. fmt.Println(viper.Get("hostname")) // myhostname.com

Firestore

  1. viper.AddRemoteProvider("firestore", "google-cloud-project-id", "collection/document")
  2. viper.SetConfigType("json") // Config's format: "json", "toml", "yaml", "yml"
  3. err := viper.ReadRemoteConfig()

当然,您也可以使用

远程密钥/值存储示例-加密

  1. viper.AddSecureRemoteProvider("etcd","http://127.0.0.1:4001","/config/hugo.json","/etc/secrets/mykeyring.gpg")
  2. viper.SetConfigType("json") // because there is no file extension in a stream of bytes, supported extensions are "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv"
  3. err := viper.ReadRemoteConfig()

观看etcd的变化-未加密

  1. // alternatively, you can create a new viper instance.
  2. var runtime_viper = viper.New()
  3. runtime_viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001", "/config/hugo.yml")
  4. runtime_viper.SetConfigType("yaml") // because there is no file extension in a stream of bytes, supported extensions are "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv"
  5. // read from remote config the first time.
  6. err := runtime_viper.ReadRemoteConfig()
  7. // unmarshal config
  8. runtime_viper.Unmarshal(&runtime_conf)
  9. // open a goroutine to watch remote changes forever
  10. go func(){
  11. for {
  12. time.Sleep(time.Second * 5) // delay after each request
  13. // currently, only tested with etcd support
  14. err := runtime_viper.WatchRemoteConfig()
  15. if err != nil {
  16. log.Errorf("unable to read remote config: %v", err)
  17. continue
  18. }
  19. // unmarshal new config into our runtime config struct. you can also use channel
  20. // to implement a signal to notify the system of the changes
  21. runtime_viper.Unmarshal(&runtime_conf)
  22. }
  23. }()

从viper中获取值

在Viper中,有几种方法可以根据值的类型获取值。存在以下功能和方法:

  • Get(key string) : interface{}
  • GetBool(key string) : bool
  • GetFloat64(key string) : float64
  • GetInt(key string) : int
  • GetIntSlice(key string) : []int
  • GetString(key string) : string
  • GetStringMap(key string) : map[string]interface{}
  • GetStringMapString(key string) : map[string]string
  • GetStringSlice(key string) : []string
  • GetTime(key string) : time.Time
  • GetDuration(key string) : time.Duration
  • IsSet(key string) : bool
  • AllSettings() : map[string]interface{}

需要注意的一件重要的事情是,如果没有找到每个Get函数将返回一个零值。为了检查给定的键是否存在,提供了IsSet()方法。
例子:

  1. viper.GetString("logfile") // case-insensitive Setting & Getting
  2. if viper.GetBool("verbose") {
  3. fmt.Println("verbose enabled")
  4. }

访问嵌套keys

访问器方法还接受深度嵌套键的格式化路径。例如,如果下面的JSON文件被加载:

  1. {
  2. "host": {
  3. "address": "localhost",
  4. "port": 5799
  5. },
  6. "datastore": {
  7. "metric": {
  8. "host": "127.0.0.1",
  9. "port": 3099
  10. },
  11. "warehouse": {
  12. "host": "198.0.0.1",
  13. "port": 2112
  14. }
  15. }
  16. }

Viper可以通过传递a来访问嵌套字段。键的分隔路径:

  1. GetString("datastore.metric.host") // (returns "127.0.0.1")

这遵守了上面建立的优先规则;对路径的搜索将依次遍历剩余的配置注册中心,直到找到为止。

例如,给定这个配置文件,datastore.metric。主机和datastore.metric。端口已经定义(可能会被覆盖)。如果加上datastore.metric。协议是默认定义的,Viper也会找到它。

然而,如果数据存储。度量被覆盖(由一个标志、一个环境变量、Set()方法,…)和一个立即值,然后是数据存储的所有子键。度量变得没有定义,它们被高优先级配置级别“隐藏”了。

Viper可以通过在路径中使用数字来访问数组索引。例如:

  1. {
  2. "host": {
  3. "address": "localhost",
  4. "ports": [
  5. 5799,
  6. 6029
  7. ]
  8. },
  9. "datastore": {
  10. "metric": {
  11. "host": "127.0.0.1",
  12. "port": 3099
  13. },
  14. "warehouse": {
  15. "host": "198.0.0.1",
  16. "port": 2112
  17. }
  18. }
  19. }
  20. GetInt("host.ports.1") // returns 6029

最后,如果存在匹配分隔键路径的键,则返回其值。如。

  1. {
  2. "datastore.metric.host": "0.0.0.0",
  3. "host": {
  4. "address": "localhost",
  5. "port": 5799
  6. },
  7. "datastore": {
  8. "metric": {
  9. "host": "127.0.0.1",
  10. "port": 3099
  11. },
  12. "warehouse": {
  13. "host": "198.0.0.1",
  14. "port": 2112
  15. }
  16. }
  17. }
  18. GetString("datastore.metric.host") // returns "0.0.0.0"

提取子树

在开发可重用模块时,提取配置的子集并将其传递给模块通常很有用。通过这种方式,可以使用不同的配置对模块进行多次实例化。
例如,一个应用程序可能会为了不同的目的使用多个不同的缓存存储:

  1. cache:
  2. cache1:
  3. max-items: 100
  4. item-size: 64
  5. cache2:
  6. max-items: 200
  7. item-size: 80

我们可以将缓存名传递给模块(例如。但是它需要奇怪的连接来访问配置键,并且与全局配置之间的分离也会更少。

因此,我们不这样做,而是将一个Viper实例传递给构造函数,它表示配置的一个子集:

  1. cache1Config := viper.Sub("cache.cache1")
  2. if cache1Config == nil { // Sub returns nil if the key cannot be found
  3. panic("cache configuration not found")
  4. }
  5. cache1 := NewCache(cache1Config)

注意:总是检查子的返回值。如果找不到键,它会返回nil。
内部,NewCache函数可以直接地址最大项目和项目大小的键:

  1. func NewCache(v *Viper) *Cache {
  2. return &Cache{
  3. MaxItems: v.GetInt("max-items"),
  4. ItemSize: v.GetInt("item-size"),
  5. }
  6. }

生成的代码很容易测试,因为它从主配置结构中解耦,而且更容易重用(出于同样的原因)。

反序列化

您还可以选择将所有值或特定值解封到结构体、映射等。
有两种方法可以做到这一点:

  • Unmarshal(rawVal interface{}) : error
  • UnmarshalKey(key string, rawVal interface{}) : error

例子:

  1. type config struct {
  2. Port int
  3. Name string
  4. PathMap string `mapstructure:"path_map"`
  5. }
  6. var C config
  7. err := viper.Unmarshal(&C)
  8. if err != nil {
  9. t.Fatalf("unable to decode into struct, %v", err)
  10. }

如果你想把键本身包含点(默认的键分隔符)的配置拆编,你必须改变分隔符:

  1. v := viper.NewWithOptions(viper.KeyDelimiter("::"))
  2. v.SetDefault("chart::values", map[string]interface{}{
  3. "ingress": map[string]interface{}{
  4. "annotations": map[string]interface{}{
  5. "traefik.frontend.rule.type": "PathPrefix",
  6. "traefik.ingress.kubernetes.io/ssl-redirect": "true",
  7. },
  8. },
  9. })
  10. type config struct {
  11. Chart struct{
  12. Values map[string]interface{}
  13. }
  14. }
  15. var C config
  16. v.Unmarshal(&C)

Viper还支持反编组到嵌入式结构:

  1. /*
  2. Example config:
  3. module:
  4. enabled: true
  5. token: 89h3f98hbwf987h3f98wenf89ehf
  6. */
  7. type config struct {
  8. Module struct {
  9. Enabled bool
  10. moduleConfig `mapstructure:",squash"`
  11. }
  12. }
  13. // moduleConfig could be in a module specific package
  14. type moduleConfig struct {
  15. Token string
  16. }
  17. var C config
  18. err := viper.Unmarshal(&C)
  19. if err != nil {
  20. t.Fatalf("unable to decode into struct, %v", err)
  21. }

Viper在后台使用github.com/mitchellh/mapstructure来解封值,默认情况下使用mapstructure标记。
注意 当我们需要将viper读取的配置反序列到我们定义的结构体变量中时,一定要使用mapstructuretag哦!

序列化成字符串

您可能需要将viper中保存的所有设置编组为一个字符串,而不是将它们写入文件。您可以使用您最喜欢的格式的marshaller配置返回的AllSettings()。

  1. import (
  2. yaml "gopkg.in/yaml.v2"
  3. // ...
  4. )
  5. func yamlStringSettings() string {
  6. c := viper.AllSettings()
  7. bs, err := yaml.Marshal(c)
  8. if err != nil {
  9. log.Fatalf("unable to marshal config to YAML: %v", err)
  10. }
  11. return string(bs)
  12. }

Viper or Vipers?

已经准备好使用了。开始使用Viper不需要配置或初始化。由于大多数应用程序都希望使用单个中央存储库进行配置,因此viper包提供了这一点。它类似于单例。

在上面的所有示例中,它们都演示了使用viper的单例方式。

使用多个viper实例

您还可以创建许多不同的蝰蛇用于您的应用程序。每一个都有自己独特的配置和值集。每个都可以从不同的配置文件、键值存储等等读取。viper包支持的所有函数都镜像为viper上的方法。

例子:

  1. x := viper.New()
  2. y := viper.New()
  3. x.SetDefault("ContentDir", "content")
  4. y.SetDefault("ContentDir", "foobar")
  5. //...

当工作与多个viper实例,它是由用户保持跟踪不同的viper。