什么是Viper
Viper是go应用程序的完全配置解决方案。可以处理所有类型和格式的配置需求。支持:
- 设置默认值
- 从JSON,TOML,YAML,HCL,envfile和Java属性配置文件中读取。
- 监控和重新读取配置文件(可选的)。
- 从远程配置系统(etcd或者Consul)读取,并监控变化。
- 从命令行选项读取。
- 从buffer中读取。
- 设置明确的值。
可以将Viper视为满足所有应用程序配置需求的注册表。
Viper的优点
Viper可以实现如下功能:
- 找到,加载并解析JSON,TOML,YAML,HCL,INI,envfile和Java属性格式的配置文件。
- 提供为不同的配置选项设置默认值的机制。
- 提供一种机制来让命令行标志指定的选项的值覆盖对应的值。
- 提供了一个别名系统,能够在不破坏现有代码的情况下重命名参数。
- 用户提供的命令行或者配置文件与默认值相同时,能够容易识别出不同。
Viper中的优先级从高到低为:
- 显式调用
Set。 - flag。
- env。
- config。
- key/value store
- default。
Note
Viper的配置键是不区分大小写的,正在考虑是否设置为可选项。
将值存入Viper
建立默认值
一个好的配置系统需要支持默认值。在一个键没有在配置文件,环境变量,远程配置或者flag中设置的时候非常有用。 示例:
viper.SetDefault("ContentDir", "content")
viper.SetDefault("LayoutDir", "layouts")
viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "categories"})读取配置文件
Viper支持JSON,TOML,YAML,HCL,INI,envfile和Java属性文件。 Viper可以搜索多条路径,但是只有一个Viper实例只支持一个配置文件。Viper没有默认的配置搜索路径,需要应用程序设置。
下面有一个使用Viper搜索和读取配置文件的示例,没有一个特定的路径是必需的,但是在需要配置文件的地方至少应该提供一个路径。
// 配置文件名
viper.SetConfigName("config")
// 如果配置文件名中没有扩展名,需要指定扩展名
viper.SetConfigType("yaml")
// 设置搜索路径
viper.AddConfigPath("/etc/appname/")
viper.AddConfigPath("$HOME/.appname")
viper.AddConfigPath(".")
// 查找和读取配置文件
err := viper.ReadInConfig()
if err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
} else {
}
}Note
实际的配置文件可以没有扩展名,在代码中指明文件格式即可。
写配置文件
存储在运行过程中的修改。可以使用一系列命令:
WriteConfig:如果存在预定义的路径,将当前的Viper配置写入。如果没有预定义的路径,则错误。如果存在当前的配置文件,会将其覆盖。SafeWriteConfig:将当前的viper配置写入预定义路径,不存在预定义路径则出错。如果当前配置文件存在,不会将其覆盖WriteConfigAs:将当前的viper写入给定的文件路径。给定的文件如果存在则会覆盖。SafeWriteConfigAs:将当前的viper配置写入给定的文件路径。给定文件存在不会覆盖。 就是说,以”Safe”开头的不会覆盖文件。文件不存在会创建。 示例:
viper.WriteConfig()
viper.SafeWriteConfig()
viper.WriteConfigAs("/path/to/my/.config")
viper.SafeWriteConfigAs("/path/to/my/.config") //已经存在会报错
viper.SafeWriteConfigAs("/path/to/my/.other_config")监控与重读配置文件
viper支持在运行中实时读取一个配置文件的能力。 不必通过重启应用来让配置生效。 只需告诉viper实例watchConfig。您还可以为Viper提供一个函数,以便在每次发生更改时运行。
viper.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("Config file changed:", e.Name)
})
viper.WatchConfig()从io.Reader中读取配置
Viper预先定义了许多配置源,例如文件,环境变量,标志和远程K/V存储,但不配置它们。您还可以实现自己所需的配置源并将其给到Viper。
viper.SetConfigType("yaml") // or viper.SetConfigType("YAML")
// 配置
var yamlExample = []byte(`
Hacker: true
name: steve
hobbies:
- skateboarding
- snowboarding
- go
clothing:
jacket: leather
trousers: denim
age: 35
eyes : brown
beard: true
`)
// 从buffer读取配置
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)
viper.Set("loud", true)
viper.GetBool("loud")
viper.GetBool("verbose")使用环境变量
Viper对环境变量有完善的支持,有以下五个方法:
AutomaticEnv()BindEnv(string ...) : errorSetEnvPrefix(string)SetEnvKeyReplace(string...) *strings.ReplacerAllowEmptyEnv(bool)在处理ENV变量时,务必认识到Viper将ENV变量视为区分大小写的。
环境变量都是独一无二的。使用SetEnvPrefix之后,Viper读取环境变量就会使用设置的前缀了,BindEnv和AutomaticEnv也会使用设置的前缀。
BindEnv接受多个参数,第一个参数是键名,其余的是绑定到这个键的环境变量,按照指定的顺序优先。如果没有指定环境变量,那么会自动去找prefix+_+key全大写的的环境变量。
AutomaticEnv与SetEnvPrefix结合非常强大。调用它的时候,viper就会进行环境变量检查并发起viper.Get,会将环境变量的前缀与全大写的键名作为环境变量名。
示例:
// 设置前缀
viper.SetEnvPrefix("spf")
// 没有指定环境变量,将prefix_key全大写作为绑定的环境变量
viper.BindEnv("id")
os.Setenv("SPF_ID", "13")
id := viper.Get("id") // 13使用Flags
Viper通过Pflags支持绑定选项。
就像BindEnv,值不是在调用该函数的时候绑定的,而是访问值的时候绑定的,意味着可以尽早绑定。
可以使用BindPFlag绑定单个flag。示例:
pflag.Int("port", 1138, "Port to run Application server on")
viper.BindPFlag("port", pflag.Lookup("port"))
t.Log(viper.Get("port"))也可以绑定pflags集合:
pflag.Int("port", 1138, "Port to run Application server on")
pflag.Int("flagname", 1234, "help message for flagname")
pflag.Parse()
viper.BindPFlags(pflag.CommandLine)
t.Log(viper.Get("port"))
t.Log(viper.Get("flagname"))使用pflag并不会排斥使用了标准库中flag库的其它包,pflag包可以通过导入标志来处理flag包的标志,通过调用pflag包提供的AddGoFlagSet即可完成。示例如下:
flag.Int("flagname", 1234, "help message for flagname")
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
pflag.Parse()
viper.BindPFlags(pflag.CommandLine)
t.Log(viper.GetInt("flagname"))如果不想使用pflags,viper提供了两个Go接口来绑定其它flag系统,只要实现FlagValue接口,就可以调用
viper.BindFlagValue("my-flag-name", myFlag{})进行绑定。
实现FlagValueSet接口,可以绑定FlagValue的集合。调用viper.BindFlagValues即可。
远程K/V存储支持
只需要空白导入viper/remote即可开启远程支持。
import _ "github.com/spf13/viper/remote"viper将会从远程的键值存储例如etcd或者Consul的路径上读取配置字符串(JSON,TOML,YAML,HCL或者envfile)。
优先级要高于默认值,但比其它几种方式要低。
viper使用crypt来从KV存储中获取配置,意味着是可以进行自动加密和解密的。这是个可选项。
远程读取的示例为:
viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001","/config/hugo.json")
viper.SetConfigType("json")
err := viper.ReadRemoteConfig()如果需要加密,只需要额外一个参数,指明加密文件。
viper.AddSecureRemoteProvider("etcd","http://127.0.0.1:4001","/config/hugo.json","/etc/secrets/mykeyring.gpg")
viper.SetConfigType("json")
err := viper.ReadRemoteConfig()从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{}GetStringSlice(key string) []stringGetTime(key string) time.TimeGetDuration(key string) time.DurationIsSet(key string) boolAllSettings() map[string]interface{}
Note
如果没有找到,Get函数会返回零值。
IsSet()方法会检测是否存在给定的键
示例:
viper.GetString("logfile")
if viper.GetBool("verbose") {
fmt.Println("verbose enabled")
}访问嵌套的键
访问方法支持嵌套的键的访问,例如下面的JSON文件
{
"host": {
"address": "localhost",
"port": 5799
},
"datastore": {
"metric": {
"host": "127.0.0.1",
"port": 3099
},
"warehouse": {
"host": "198.0.0.1",
"port": 2112
}
}
}viper通过.作为键的路径分隔符来访问嵌套的字段:
GetString("datastore.metric.host")对于数组直接通过下标即可访问。
{
"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存储。
cache:
cache1:
max-items: 100
item-size: 64
cache2:
max-items: 200
item-size: 80通过将配置传递给子模块可以实现与全局配置的分离
cache1Config := viper.Sub("cache.cache1")
if cache1Config == nil { // Sub returns nil if the key cannot be found
panic("cache configuration not found")
}
cache1 := NewCache(cache1Config)Unmarshaling
还可以选择将指定值或者全部值unmarshaling为结构体,map等等。 有两个函数可以实现这点:
Unmarshal(rawVal interface{}) errorUnmarshalKey(key string, rawVal interface{}) error
示例:
type config struct {
Port int
Name string
PathMap string `mapstructure:"path_map"`
}
var C config
err := 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 config
v.Unmarshal(&C)Viper还支持将数据解构到嵌入的结构中
/*
Example config:
module:
enabled: true
token: 89h3f98hbwf987h3f98wenf89ehf
*/
type config struct {
Module struct {
Enabled bool
moduleConfig `mapstructure:",squash"`
}
}
// moduleConfig could be in a module specific package
type moduleConfig struct {
Token string
}
var C config
err := viper.Unmarshal(&C)
if err != nil {
t.Fatalf("unable to decode into struct, %v", err)
}Viper在使用github.com/mitchellh/mapstructure,使用mapstructure作为解构值时的默认标签。
解码自定义格式
viper经常被请求支持更多格式的值和解码器,例如解析以点,逗号,分号等等作为分隔符的字符串为切片。 这已经在Viper中使用mapstructure解码hooks实现了。
编码到string
可能需要将viper的所有设置都编码到一个字符串而不是写入文件,可以使用你最喜欢的格式编码器来将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
您还可以创建许多不同的viper用于您的应用程序。每个都有自己独特的配置和值集。每个可以读取不同的配置文件,键值存储,等等。viper包支持的所有函数都映射为为viper上的方法。 示例:
x := viper.New()
y := viper.New()
x.SetDefault("ContentDir", "content")
y.SetDefault("ContentDir", "foobar")
//...