安装
go get github.com/spf13/viper
Viper是什么?
viper是一个完整的配置解决方案的Go应用程序,包括12个因素的应用程序。它被设计为在应用程序中工作,并且可以处理所有类型的配置需求和格式。它支持:
- 设置默认值
- 读取JSON, TOML, YAML, HCL, envfile和Java属性配置文件
- 实时查看和重新读取配置文件(可选)
- 从环境变量中读取
- 从远程配置系统(etcd或领事)读取,并观察变化
- 从命令行标志读取
- 读取缓冲区
- 设置明确的值
可以将Viper看作您所有应用程序配置需要的注册中心。
为什么选择Viper?
在构建一个现代应用程序时,您不需要担心配置文件格式;你要专注于开发出色的软件。viper会来帮忙的。
viper为您做以下工作:
- 查找、加载和解编JSON、TOML、YAML、HCL、INI、envfile或Java属性格式的配置文件。
- 提供一种机制来为不同的配置选项设置默认值。
- 提供一种机制,为通过命令行标志指定的选项设置覆盖值。
- 提供别名系统,以便在不破坏现有代码的情况下轻松重命名参数。
- 当用户提供了与默认配置相同的命令行或配置文件时,很容易区分。
viper使用以下优先顺序。每个项目优先于它下面的项目:
- explicit call to
Set - flag
- env
- config
- key/value store
- default
重要:viper配置键不区分大小写。目前正在讨论是否将其作为可选项。
**
赋值
建立默认值
一个好的配置系统将支持默认值。键不需要默认值,但是在没有通过配置文件、环境变量、远程配置或标志设置键的情况下,它很有用。
例子:
viper.SetDefault("ContentDir", "content")viper.SetDefault("LayoutDir", "layouts")viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "categories"})
读取配置文件
Viper需要最小的配置,所以它知道在哪里查找配置文件。Viper支持JSON, TOML, YAML, HCL, INI, envfile和Java属性文件。Viper可以搜索多个路径,但目前单个Viper实例只支持单个配置文件。Viper不默认任何配置搜索路径,将默认决策留给应用程序。
下面是如何使用Viper搜索和读取配置文件的示例。不需要任何特定的路径,但是在需要配置文件的地方至少应该提供一个路径。
viper.SetConfigName("config") // name of config file (without extension)viper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the nameviper.AddConfigPath("/etc/appname/") // path to look for the config file inviper.AddConfigPath("$HOME/.appname") // call multiple times to add many search pathsviper.AddConfigPath(".") // optionally look for config in the working directoryerr := viper.ReadInConfig() // Find and read the config fileif err != nil { // Handle errors reading the config filepanic(fmt.Errorf("Fatal error config file: %s \n", err))}
你可以像这样处理没有配置文件的情况:
if err := viper.ReadInConfig(); err != nil {if _, ok := err.(viper.ConfigFileNotFoundError); ok {// Config file not found; ignore error if desired} else {// Config file was found but another error was produced}}// Config file found and successfully parsed
注意[自1.6]:你也可以有一个没有扩展名的文件,并指定程序格式。对于那些位于用户主服务器中没有.bashrc之类扩展名的配置文件
写入配置文件
从配置文件中读取是有用的,但有时您希望存储在运行时所做的所有修改。为此,有一堆命令可用,每个都有自己的用途:
- WriteConfig——如果存在,则将当前的viper配置写入预定义的路径。如果没有预定义的路径,则出错。将覆盖当前配置文件(如果存在)。
- SafeWriteConfig—将当前的viper配置写入预定义的路径。如果没有预定义的路径,则出错。不会覆盖当前配置文件(如果存在)。
- WriteConfigAs——将当前的viper配置写入给定的文件路径。将覆盖给定文件(如果存在)。
- SafeWriteConfigAs——将当前的viper配置写入给定的文件路径。不会覆盖给定文件(如果存在)。
作为一个经验法则,所有标记为safe的文件都不会覆盖任何文件,如果不存在就创建,而默认行为是创建或截断。
一个小示例部分:
viper.WriteConfig() // writes current config to predefined path set by 'viper.AddConfigPath()' and 'viper.SetConfigName'viper.SafeWriteConfig()viper.WriteConfigAs("/path/to/my/.config")viper.SafeWriteConfigAs("/path/to/my/.config") // will error since it has already been writtenviper.SafeWriteConfigAs("/path/to/my/.other_config")
查看和重读配置文件
Viper支持让您的应用程序在运行时实时读取配置文件。
需要重新启动服务器以使配置生效的日子已经一去不复返了,viper驱动的应用程序可以在运行时读取配置文件的更新,不会错过任何一个节奏。
只需告诉viper实例watchConfig。还可以为Viper提供一个函数,以便在每次发生更改时运行。
确保在调用WatchConfig()之前添加了所有的配置路径。
viper.WatchConfig()viper.OnConfigChange(func(e fsnotify.Event) {fmt.Println("Config file changed:", e.Name)})
从io.Reader中读取配置
Viper预先定义了许多配置源,比如文件、环境变量、标志和远程K/V存储,但是您没有绑定到它们。您还可以实现自己所需的配置源文件,并将其提供给viper。
viper.SetConfigType("yaml") // or viper.SetConfigType("YAML")// any approach to require this configuration into your program.var yamlExample = []byte(`Hacker: truename: stevehobbies:- skateboarding- snowboarding- goclothing:jacket: leathertrousers: denimage: 35eyes : brownbeard: true`)viper.ReadConfig(bytes.NewBuffer(yamlExample))viper.Get("name") // this would be "steve"
覆盖设置
这些可能来自命令行标志,也可能来自您自己的应用程序逻辑。
viper.Set("Verbose", true)viper.Set("LogFile", LogFile)
注册和使用别名
别名允许多个键引用一个值
viper.RegisterAlias("loud", "Verbose")viper.Set("verbose", true) // same result as next lineviper.Set("loud", true) // same result as prior lineviper.GetBool("loud") // trueviper.GetBool("verbose") // true
使用环境变量
Viper完全支持环境变量。这使得12个因素的应用程序开箱。有五种方法可以帮助您与ENV合作:
AutomaticEnv()BindEnv(string...) : errorSetEnvPrefix(string)SetEnvKeyReplacer(string...) *strings.ReplacerAllowEmptyEnv(bool)
在处理ENV变量时,务必认识到Viper将ENV变量视为大小写敏感变量。
Viper提供了一种机制来确保环境变量是唯一的。通过使用SetEnvPrefix,您可以告诉Viper在读取环境变量时使用前缀。BindEnv和AutomaticEnv**都将使用这个前缀。
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 例子
SetEnvPrefix("spf") // will be uppercased automaticallyBindEnv("id")os.Setenv("SPF_ID", "13") // typically done outside of the appid := Get("id") // 13
使用Flags
viper有绑定flags的能力。具体来说,Viper支持Cobra中使用的Pflags。
与BindEnv一样,该值不是在调用绑定方法时设置的,而是在访问该方法时设置的。这意味着您可以尽可能早地进行绑定,即使是在init()函数中。
对于单个flags,BindPFlag()方法提供了此功能。
例子:
serverCmd.Flags().Int("port", 1138, "Port to run Application server on")viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))
您也可以绑定一个现有的pflag(pflag.FlagSet):
例子:
pflag.Int("flagname", 1234, "help message for flagname")pflag.Parse()viper.BindPFlags(pflag.CommandLine)i := viper.GetInt("flagname") // retrieve values from viper instead of pflag
在Viper中使用pflag并不排除使用标准库中标记包的其他包的使用。pflag包可以通过导入这些标志来处理为标志包定义的标志。这是通过调用pflag包提供的一个方便函数AddGoFlagSet()来完成的。
例子:
package mainimport ("flag""github.com/spf13/pflag")func main() {// using standard library "flag" packageflag.Int("flagname", 1234, "help message for flagname")pflag.CommandLine.AddGoFlagSet(flag.CommandLine)pflag.Parse()viper.BindPFlags(pflag.CommandLine)i := viper.GetInt("flagname") // retrieve value from viper...}
Flag 接口
如果不使用Pflags, Viper提供了两个Go接口来绑定其他标记系统。
FlagValue表示单个标志。这是一个非常简单的例子,如何实现这个接口:
type myFlag struct {}func (f myFlag) HasChanged() bool { return false }func (f myFlag) Name() string { return "my-flag-name" }func (f myFlag) ValueString() string { return "my-flag-value" }func (f myFlag) ValueType() string { return "string" }
一旦你的flag实现了这个接口,你可以简单地告诉Viper来绑定它:
viper.BindFlagValue("my-flag-name", myFlag{})
FlagValueSet表示一组标志。这是一个非常简单的例子,如何实现这个接口:
type myFlagSet struct {flags []myFlag}func (f myFlagSet) VisitAll(fn func(FlagValue)) {for _, flag := range flags {fn(flag)}}
一旦你的flag设置实现了这个接口,你可以简单地告诉Viper绑定它:
fSet := myFlagSet{flags: []myFlag{myFlag{}, myFlag{}},}viper.BindFlagValues("my-flags", fSet)
远程key/value存储支持
要在Viper中启用远程支持,请空白导入Viper /remote包:
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。
$ go get github.com/bketelsen/crypt/bin/crypt$ crypt set -plaintext /config/hugo.json /Users/hugo/settings/config.json
确认您的值已设置:
$ crypt get -plaintext /config/hugo.json
有关如何设置加密值或如何使用领事的示例,请参阅crypt文档。
远程密钥/值存储示例-未加密
etcd
viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001","/config/hugo.json")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"err := viper.ReadRemoteConfig()
Consul
您需要用包含所需配置的JSON值设置一个键来执行键/值存储。例如,创建一个具有值的代理键/值存储键MY_CONSUL_KEY:
{"port": 8080,"hostname": "myhostname.com"}
viper.AddRemoteProvider("consul", "localhost:8500", "MY_CONSUL_KEY")viper.SetConfigType("json") // Need to explicitly set this to jsonerr := viper.ReadRemoteConfig()fmt.Println(viper.Get("port")) // 8080fmt.Println(viper.Get("hostname")) // myhostname.com
Firestore
viper.AddRemoteProvider("firestore", "google-cloud-project-id", "collection/document")viper.SetConfigType("json") // Config's format: "json", "toml", "yaml", "yml"err := viper.ReadRemoteConfig()
当然,您也可以使用
远程密钥/值存储示例-加密
viper.AddSecureRemoteProvider("etcd","http://127.0.0.1:4001","/config/hugo.json","/etc/secrets/mykeyring.gpg")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"err := viper.ReadRemoteConfig()
观看etcd的变化-未加密
// alternatively, you can create a new viper instance.var runtime_viper = viper.New()runtime_viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001", "/config/hugo.yml")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"// read from remote config the first time.err := runtime_viper.ReadRemoteConfig()// unmarshal configruntime_viper.Unmarshal(&runtime_conf)// open a goroutine to watch remote changes forevergo func(){for {time.Sleep(time.Second * 5) // delay after each request// currently, only tested with etcd supporterr := runtime_viper.WatchRemoteConfig()if err != nil {log.Errorf("unable to read remote config: %v", err)continue}// unmarshal new config into our runtime config struct. you can also use channel// to implement a signal to notify the system of the changesruntime_viper.Unmarshal(&runtime_conf)}}()
从viper中获取值
在Viper中,有几种方法可以根据值的类型获取值。存在以下功能和方法:
Get(key string) : interface{}GetBool(key string) : boolGetFloat64(key string) : float64GetInt(key string) : intGetIntSlice(key string) : []intGetString(key string) : stringGetStringMap(key string) : map[string]interface{}GetStringMapString(key string) : map[string]stringGetStringSlice(key string) : []stringGetTime(key string) : time.TimeGetDuration(key string) : time.DurationIsSet(key string) : boolAllSettings() : map[string]interface{}
需要注意的一件重要的事情是,如果没有找到每个Get函数将返回一个零值。为了检查给定的键是否存在,提供了IsSet()方法。
例子:
viper.GetString("logfile") // case-insensitive Setting & Gettingif viper.GetBool("verbose") {fmt.Println("verbose enabled")}
访问嵌套keys
访问器方法还接受深度嵌套键的格式化路径。例如,如果下面的JSON文件被加载:
{"host": {"address": "localhost","port": 5799},"datastore": {"metric": {"host": "127.0.0.1","port": 3099},"warehouse": {"host": "198.0.0.1","port": 2112}}}
Viper可以通过传递a来访问嵌套字段。键的分隔路径:
GetString("datastore.metric.host") // (returns "127.0.0.1")
这遵守了上面建立的优先规则;对路径的搜索将依次遍历剩余的配置注册中心,直到找到为止。
例如,给定这个配置文件,datastore.metric。主机和datastore.metric。端口已经定义(可能会被覆盖)。如果加上datastore.metric。协议是默认定义的,Viper也会找到它。
然而,如果数据存储。度量被覆盖(由一个标志、一个环境变量、Set()方法,…)和一个立即值,然后是数据存储的所有子键。度量变得没有定义,它们被高优先级配置级别“隐藏”了。
Viper可以通过在路径中使用数字来访问数组索引。例如:
{"host": {"address": "localhost","ports": [5799,6029]},"datastore": {"metric": {"host": "127.0.0.1","port": 3099},"warehouse": {"host": "198.0.0.1","port": 2112}}}GetInt("host.ports.1") // returns 6029
最后,如果存在匹配分隔键路径的键,则返回其值。如。
{"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") // returns "0.0.0.0"
提取子树
在开发可重用模块时,提取配置的子集并将其传递给模块通常很有用。通过这种方式,可以使用不同的配置对模块进行多次实例化。
例如,一个应用程序可能会为了不同的目的使用多个不同的缓存存储:
cache:cache1:max-items: 100item-size: 64cache2:max-items: 200item-size: 80
我们可以将缓存名传递给模块(例如。但是它需要奇怪的连接来访问配置键,并且与全局配置之间的分离也会更少。
因此,我们不这样做,而是将一个Viper实例传递给构造函数,它表示配置的一个子集:
cache1Config := viper.Sub("cache.cache1")if cache1Config == nil { // Sub returns nil if the key cannot be foundpanic("cache configuration not found")}cache1 := NewCache(cache1Config)
注意:总是检查子的返回值。如果找不到键,它会返回nil。
内部,NewCache函数可以直接地址最大项目和项目大小的键:
func NewCache(v *Viper) *Cache {return &Cache{MaxItems: v.GetInt("max-items"),ItemSize: v.GetInt("item-size"),}}
生成的代码很容易测试,因为它从主配置结构中解耦,而且更容易重用(出于同样的原因)。
反序列化
您还可以选择将所有值或特定值解封到结构体、映射等。
有两种方法可以做到这一点:
Unmarshal(rawVal interface{}) : errorUnmarshalKey(key string, rawVal interface{}) : error
例子:
type config struct {Port intName stringPathMap string `mapstructure:"path_map"`}var C configerr := viper.Unmarshal(&C)if err != nil {t.Fatalf("unable to decode into struct, %v", err)}
如果你想把键本身包含点(默认的键分隔符)的配置拆编,你必须改变分隔符:
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还支持反编组到嵌入式结构:
/*Example config:module:enabled: truetoken: 89h3f98hbwf987h3f98wenf89ehf*/type config struct {Module struct {Enabled boolmoduleConfig `mapstructure:",squash"`}}// moduleConfig could be in a module specific packagetype 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标记。
注意 当我们需要将viper读取的配置反序列到我们定义的结构体变量中时,一定要使用mapstructuretag哦!
序列化成字符串
您可能需要将viper中保存的所有设置编组为一个字符串,而不是将它们写入文件。您可以使用您最喜欢的格式的marshaller配置返回的AllSettings()。
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)}
Viper or Vipers?
已经准备好使用了。开始使用Viper不需要配置或初始化。由于大多数应用程序都希望使用单个中央存储库进行配置,因此viper包提供了这一点。它类似于单例。
在上面的所有示例中,它们都演示了使用viper的单例方式。
使用多个viper实例
您还可以创建许多不同的蝰蛇用于您的应用程序。每一个都有自己独特的配置和值集。每个都可以从不同的配置文件、键值存储等等读取。viper包支持的所有函数都镜像为viper上的方法。
例子:
x := viper.New()y := viper.New()x.SetDefault("ContentDir", "content")y.SetDefault("ContentDir", "foobar")//...
当工作与多个viper实例,它是由用户保持跟踪不同的viper。
