Viper简介Viper是一个完整的Go语言项目的配置解决方案。它可以处理所有类型的配置需求和格式,相关链接如下
包文档:https://pkg.go.dev/github.com/spf13/viper
github:https://github.com/spf13/viper
Viper的优势在构建Golang程序时可以不必担心配置文件格式而更专注于实现。
viper主要包含以下操作:
查找、加载和反序列化 “json”, “toml”, “yaml”, “yml”, “properties”, “props”, “prop”, “hcl”, " tfvars", “dotenv”, “env”, “ini” 提供一种机制来为不同的配置选项设置默认值。 提供一种机制来为通过命令行参数设置指定覆盖值。 提供别名,以在不破坏现有代码的情况下轻松重命名参数。 使区分用户何时提供与默认值相同的命令行或配置文件变得容易。 每个项目的优先级都高于它下面的项目,Viper优先顺序。
显式调用 Set
命令行参数(flag) 环境变量 配置文件 key/value存储 默认值 重要提示: Viper 配置键不区分大小写。正在进行关于使之成为可选项的讨论。
Viper使用场景设置默认值 “json”, “toml”, “yaml”, “yml”, “properties”, “props”, “prop”, “hcl”, “tfvars”, “dotenv”, “env”, " ini"文件中读取载入 实时观看和重新读取配置文件(可选) 从环境变量中读取 从远程配置系统(etcd 或 Consul)读取,并观察变化 从命令行标志读取 从缓冲区读取 设置显式值 Viper 可以被认为是满足所有应用程序配置需求的注册表
Viper的安装1 go get -u -v github.com/spf13/viper
Viper使用实例 使用默认值一个好的配置系统对于默认值拥有良好的支持,其重要性不言而喻。在Viper中的默认值使用如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package mainimport ( "fmt" "github.com/spf13/viper" ) func main () { viper.SetDefault(`Name` , `Payne` ) viper.SetDefault(`Age` , 20 ) viper.SetDefault(`hobby` , map [string ]string { `First hobby` : `sing` , `Second hobby` : `jump` , `Third hobby` : `Rap` , `fourth hobby` : `Play Basketball` , }) fmt.Println(viper.Get(`Name` )) fmt.Println(viper.Get(`Age` )) fmt.Println(viper.Get(`hobby` )) for _, i := range viper.GetStringMapString(`hobby` ) { fmt.Println(i) } }
覆盖设置这些可能来自命令行标志,也可能来自你自己的应用程序逻辑。
1 2 viper.Set("Verbose" , true ) viper.Set("LogFile" , LogFile)
注册和使用别名别名允许多个键引用单个值
1 2 3 4 5 6 7 viper.RegisterAlias("loud" , "Verbose" ) viper.Set("verbose" , true ) viper.Set("loud" , true ) viper.GetBool("loud" ) viper.GetBool("verbose" )
配置文件使用 读取配置文件抽离统一化管理成为配置文件,将所有的配置写在文件中便于管理修改与编辑。Viper 支持 “json”, “toml”, " yaml", “yml”, “properties”, “props”, “prop”, “hcl”, " tfvars", “dotenv”, “env”, “ini” 属性文件。Viper 可以搜索多个路径,但目前单个 Viper 实例仅支持单个配置文件。Viper 不会默认任何配置搜索路径,将默认决定留给应用程序。不需要任何特定路径,但应至少提供一个需要配置文件的路径。以下是如何使用 Viper 搜索和读取配置文件的示例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 viper.SetConfigFile("./config.yaml" ) viper.SetConfigName("config" ) viper.SetConfigType("yaml" ) viper.AddConfigPath("/etc/appname/" ) viper.AddConfigPath("$HOME/.appname" ) viper.AddConfigPath("." ) err := viper.ReadInConfig() if err != nil { panic (fmt.Errorf("Fatal error config file: %s \n" , err)) } if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); ok { log.Println("no such config file" ) } else { log.Println("read config error" ) } log.Fatal(err) }
注意 若采用setConfigName
则只会使用第一个配置文件夹
推荐使用SetConfigFile("path/file_name")
来完成配置文件的载入
从io.Reader读取配置Viper预先定义了许多配置源,如文件、环境变量、标志和远程K/V存储,但也可以实现自己所需的配置源并将其提供给viper。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import ( "bytes" "fmt" "github.com/spf13/viper" ) func yamlConf () { viper.SetConfigType("yaml" ) ExampleYaml := []byte (` name: Payne Age: 18 ` ) viper.ReadConfig(bytes.NewBuffer(ExampleYaml)) fmt.Println(viper.Get("NAME" )) fmt.Println(viper.Get("Age" )) } func jsonConf () { viper.SetConfigType(`json` ) ExampleJSON := []byte (`{ "name": "payne", "age": 21 }` ) viper.ReadConfig(bytes.NewBuffer(ExampleJSON)) fmt.Println(viper.Get("name" )) fmt.Println(viper.GetInt("age" )) }
写入配置文件从配置文件中读取是很有用的,但有时你想存储在运行时所作的所有修改都比较繁琐。viper提供了相关功能
WriteConfig - 将当前的viper
配置写入预定义的路径并覆盖(如果存在的话)。如果没有预定义的路径,则报错。 SafeWriteConfig - 将当前的viper
配置写入预定义的路径。如果没有预定义的路径,则报错。如果存在,将不会覆盖当前的配置文件。 WriteConfigAs - 将当前的viper
配置写入给定的文件路径。将覆盖给定的文件(如果它存在的话)。 SafeWriteConfigAs - 将当前的viper
配置写入给定的文件路径。不会覆盖给定的文件(如果它存在的话)。 根据经验,标记为safe
的所有方法都不会覆盖任何文件,而是直接创建(如果不存在),而默认行为是创建或截断。
监听配置文件Viper支持在运行时实时读取配置文件的功能。
需要重新启动服务器以使配置生效的日子已经一去不复返了,viper驱动的应用程序可以在运行时读取配置文件的更新,而不会错过任何消息。
只需告诉viper实例watchConfig。可选地,你可以为Viper提供一个回调函数,以便在每次发生更改时运行。
确保在调用WatchConfig()
之前添加了所有的配置路径。
1 2 3 4 5 viper.WatchConfig() viper.OnConfigChange(func (e fsnotify.Event) { fmt.Println("Config file changed:" , e.Name) })
实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package mainimport ( "fmt" "github.com/fsnotify/fsnotify" "github.com/spf13/viper" "log" "time" ) func main () { for { viper.SetConfigFile(`./example/config.yaml` ) if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); ok { log.Println("no such config file" ) } else { log.Println("read config error" ) } log.Fatal(err) } viper.SetDefault(`a` , `b` ) viper.WatchConfig() viper.OnConfigChange(func (e fsnotify.Event) { fmt.Println("Config file changed:" , e.Name) }) fmt.Println(viper.Get(`port` )) time.Sleep(time.Second * 2 ) } }
环境变量Viper完全支持环境变量。以下几种方法进行对ENV协作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 AllowEmptyEnv(allowEmptyEnv bool ) AutomaticEnv() func BindEnv (input ...string ) error func SetEnvPrefix (in string ) func SetEnvKeyReplacer (r *strings.Replacer)
使用ENV变量时,务必要意识到Viper将ENV变量视为区分大小写。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 i := 0 for { viper.SetDefault(`Val` , `Original` ) viper.BindEnv(`Val` ) fmt.Println(viper.Get(`Val` )) if i == 3 { os.Setenv("VAL" , "changed" ) } fmt.Println(i) i += 1 time.Sleep(1 * time.Second) } i := 0 for { viper.SetDefault(`Val` , `Original` ) viper.SetEnvPrefix(`CUSTOM` ) viper.BindEnv(`Val` ) fmt.Println(viper.Get(`Val` )) if i == 3 { os.Setenv("CUSTOM_VAL" , "changed" ) } fmt.Println(i) i += 1 time.Sleep(1 * time.Second) }
当第四次输出时VAL
,将输出change
小技巧:在使用环境变量的时候推荐采用全大写,避免混淆
使用viper获取值获取函数如下所示,具体作用见名思意
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Get(key string ) interface {} Sub(key string ) *Viper GetBool(key string ) bool GetDuration(key string ) time.Duration GetFloat64(key string ) float64 GetInt(key string ) int GetInt32(key string ) int32 GetInt64(key string ) int64 GetIntSlice(key string ) []int GetSizeInBytes(key string ) uint GetString(key string ) string GetStringMap(key string ) map [string ]interface {} GetStringMapString(key string ) map [string ]string GetStringMapStringSlice(key string ) map [string ][]string GetStringSlice(key string ) []string GetTime(key string ) time.Time GetUint(key string ) uint GetUint32(key string ) uint32 GetUint64(key string ) uint64 InConfig(key string ) bool IsSet(key string ) bool AllSettings() map [string ]interface {}
访问嵌套的键访问器方法也接受深度嵌套键的格式化路径。例如,如果加载下面的JSON文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { "host" : { "address" : "localhost" , "port" : 5799 } , "datastore" : { "metric" : { "host" : "127.0.0.1" , "port" : 3099 } , "warehouse" : { "host" : "198.21.112.32" , "port" : 2112 } } }
Viper可以通过传入.
分隔的路径来访问嵌套字段:
1 2 GetString("datastore.datastore.warehouse.host" )
这遵守上面建立的优先规则;搜索路径将遍历其余配置注册表,直到找到为止。( 译注:因为Viper支持从多种配置来源,例如磁盘上的配置文件>命令行标志位>环境变量>远程Key/Value存储> 默认值,我们在查找一个配置的时候如果在当前配置源中没找到,就会继续从后续的配置源查找,直到找到为止。)
例如,在给定此配置文件的情况下,datastore.metric.host
和datastore.metric.port
均已定义(并且可以被覆盖)。如果另外在默认值中定义了datastore.metric.protocol
,Viper也会找到它。然而,如果datastore.metric
被直接赋值覆盖(被flag,环境变量,set()
方法等等…),那么datastore.metric
的所有子键都将变为未定义状态,它们被高优先级配置级别“遮蔽”(shadowed)了。最后,如果存在与分隔的键路径匹配的键,则返回其值。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 { "datastore.metric.host" : "0.0.0.0" , "host" : { "address" : "localhost" , "port" : 5799 }, "datastore" : { "metric" : { "host" : "127.0.0.1" , "port" : 3099 }, "warehouse" : { "host" : "198.0.0.1" , "port" : 2112 } } } GetString("datastore.metric.host" )
提取子树从Viper中提取子树,viper
实例现在代表了以下配置:
1 2 3 4 5 6 7 app: cache1: max-items: 100 item-size: 64 cache2: max-items: 200 item-size: 80
执行后:
1 subv := viper.Sub("app.cache1" )
subv
现在就代表:
1 2 max-items: 100 item-size: 64
假设我们现在有这么一个函数:
1 func NewCache (cfg *Viper) *Cache {...}
它基于subv
格式的配置信息创建缓存。现在,可以轻松地分别创建这两个缓存,如下所示:
1 2 3 4 5 cfg1 := viper.Sub("app.cache1" ) cache1 := NewCache(cfg1) cfg2 := viper.Sub("app.cache2" ) cache2 := NewCache(cfg2)
反序列化你还可以选择将所有或特定的值解析到结构体、map等。
有两种方法可以做到这一点:
Unmarshal(rawVal interface{}) : error
UnmarshalKey(key string, rawVal interface{}) : error
1 2 3 4 5 6 7 8 9 10 11 12 type config struct { Port int Name string PathMap string `mapstructure:"path_map"` } var C configerr := viper.Unmarshal(&C) if err != nil { t.Fatalf("unable to decode into struct, %v" , err) }
如果你想要解析那些键本身就包含.
(默认的键分隔符)的配置,你需要修改分隔符:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 v := viper.NewWithOptions(viper.KeyDelimiter("::" )) v.SetDefault("chart::values" , map [string ]interface {}{ "ingress" : map [string ]interface {}{ "annotations" : map [string ]interface {}{ "traefik.frontend.rule.type" : "PathPrefix" , "traefik.ingress.kubernetes.io/ssl-redirect" : "true" , }, }, }) type config struct { Chart struct { Values map [string ]interface {} } } var C configv.Unmarshal(&C)
Viper还支持解析到嵌入的结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 type config struct { Module struct { Enabled bool moduleConfig `mapstructure:",squash"` } } type moduleConfig struct { Token string } var C configerr := viper.Unmarshal(&C) if err != nil { t.Fatalf("unable to decode into struct, %v" , err) }
Viper在后台使用github.com/mitchellh/mapstructure 来解析值,其默认情况下使用mapstructure
tag。
注意 当我们需要将viper读取的配置反序列到我们定义的结构体变量中时,一定要使用mapstructure
tag!
序列化成字符串你可能需要将viper中保存的所有设置序列化到一个字符串中,而不是将它们写入到一个文件中。你可以将自己喜欢的格式的序列化器与AllSettings()
返回的配置一起使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 import ( yaml "gopkg.in/yaml.v2" ) func yamlStringSettings () string { c := viper.AllSettings() bs, err := yaml.Marshal(c) if err != nil { log.Fatalf("unable to marshal config to YAML: %v" , err) } return string (bs) }
远程Key/Value存储支持在Viper中启用远程支持,需要在代码中匿名导入viper/remote
这个包。
1 import _ "github.com/spf13/viper/remote"
Viper将读取从Key/Value存储(例如etcd或Consul)中的路径检索到的配置字符串(如JSON
、TOML
、YAML
、HCL
、envfile
和Java properties
格式)。这些值的优先级高于默认值,但是会被从磁盘、flag或环境变量检索到的配置值覆盖。(译注:也就是说Viper加载配置值的优先级为:磁盘上的配置文件> 命令行标志位>环境变量>远程Key/Value存储>默认值。)
Viper使用crypt 从K/V存储中检索配置,这意味着如果你有正确的gpg密匙,你可以将配置值加密存储并自动解密。加密是可选的。
你可以将远程配置与本地配置结合使用,也可以独立使用。
crypt
有一个命令行助手,你可以使用它将配置放入K/V存储中。crypt
默认使用在http://127.0.0.1:4001 的etcd。
1 2 go get github.com/bketelsen/crypt/bin/crypt crypt set -plaintext /config/hugo.json /Users/hugo/settings/config.json
确认值已经设置:
1 crypt get -plaintext /config/hugo.json
有关如何设置加密值或如何使用Consul的示例,请参见crypt
文档。
远程Key/Value存储示例-未加密 etcd1 2 3 viper.AddRemoteProvider("etcd" , "http://127.0.0.1:4001" ,"/config/hugo.json" ) viper.SetConfigType("json" ) err := viper.ReadRemoteConfig()
Consul你需要 Consul Key/Value存储中设置一个Key保存包含所需配置的JSON值。例如,创建一个keyMY_CONSUL_KEY
将下面的值存入Consul key/value 存储:
1 2 3 viper.AddRemoteProvider("consul" , "localhost:8500" , "MY_CONSUL_KEY" ) viper.SetConfigType("json" ) err: = viper.ReadRemoteConfig()
Firestore1 2 3 viper.AddRemoteProvider("firestore" , "google-cloud-project-id" , "collection/document" ) viper.SetConfigType("json" ) err := viper.ReadRemoteConfig()
当然,你也可以使用SecureRemoteProvider
。
远程Key/Value存储示例-加密1 2 3 viper.AddSecureRemoteProvider("etcd" ,"http://127.0.0.1:4001" ,"/config/hugo.json" ,"/etc/secrets/mykeyring.gpg" ) viper.SetConfigType("json" ) err := viper.ReadRemoteConfig()
监控etcd中的更改-未加密1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 var runtime_viper = viper.New()runtime_viper.AddRemoteProvider("etcd" , "http://127.0.0.1:4001" , "/config/hugo.yml" ) runtime_viper.SetConfigType("yaml" ) err := runtime_viper.ReadRemoteConfig() runtime_viper.Unmarshal(&runtime_conf) go func () { for { time.Sleep(time.Second * 5 ) err := runtime_viper.WatchRemoteConfig() if err != nil { log.Errorf("unable to read remote config: %v" , err) continue } runtime_viper.Unmarshal(&runtime_conf) } }()
基于Viper实现的环境变量动态链接1 2 3 4 5 6 7 8 9 10 11 import ( "github.com/spf13/viper" ) func DynamicEnv (envName, Prefix string , defaultVal interface {}) interface {} { viper.SetDefault(envName, defaultVal) viper.SetEnvPrefix(Prefix) viper.BindEnv(envName) return viper.Get(envName) }