安装
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 name
viper.AddConfigPath("/etc/appname/") // path to look for the config file in
viper.AddConfigPath("$HOME/.appname") // call multiple times to add many search paths
viper.AddConfigPath(".") // optionally look for config in the working directory
err := viper.ReadInConfig() // Find and read the config file
if err != nil { // Handle errors reading the config file
panic(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 written
viper.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: true
name: steve
hobbies:
- skateboarding
- snowboarding
- go
clothing:
jacket: leather
trousers: denim
age: 35
eyes : brown
beard: 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 line
viper.Set("loud", true) // same result as prior line
viper.GetBool("loud") // true
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在读取环境变量时使用前缀。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 automatically
BindEnv("id")
os.Setenv("SPF_ID", "13") // typically done outside of the app
id := 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 main
import (
"flag"
"github.com/spf13/pflag"
)
func main() {
// using standard library "flag" package
flag.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 json
err := viper.ReadRemoteConfig()
fmt.Println(viper.Get("port")) // 8080
fmt.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 config
runtime_viper.Unmarshal(&runtime_conf)
// open a goroutine to watch remote changes forever
go func(){
for {
time.Sleep(time.Second * 5) // delay after each request
// currently, only tested with etcd support
err := 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 changes
runtime_viper.Unmarshal(&runtime_conf)
}
}()
从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()方法。
例子:
viper.GetString("logfile") // case-insensitive Setting & Getting
if 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: 100
item-size: 64
cache2:
max-items: 200
item-size: 80
我们可以将缓存名传递给模块(例如。但是它需要奇怪的连接来访问配置键,并且与全局配置之间的分离也会更少。
因此,我们不这样做,而是将一个Viper实例传递给构造函数,它表示配置的一个子集:
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)
注意:总是检查子的返回值。如果找不到键,它会返回nil。
内部,NewCache函数可以直接地址最大项目和项目大小的键:
func NewCache(v *Viper) *Cache {
return &Cache{
MaxItems: v.GetInt("max-items"),
ItemSize: v.GetInt("item-size"),
}
}
生成的代码很容易测试,因为它从主配置结构中解耦,而且更容易重用(出于同样的原因)。
反序列化
您还可以选择将所有值或特定值解封到结构体、映射等。
有两种方法可以做到这一点:
Unmarshal(rawVal interface{}) : error
UnmarshalKey(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读取的配置反序列到我们定义的结构体变量中时,一定要使用mapstructure
tag哦!
序列化成字符串
您可能需要将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。