介绍一个轻便好用的 Golang 配置库 Viper,项目地址:https://github.com/spf13/viper,现在有很多 Go 语言项目用到了 Viper,比如Hugo、EMC RexRay、Docker Notary、doctl等,在 github 上 start 达到7600多,可见受欢迎程度。
什么是Viper
Viper是一个方便 Go 语言应用程序处理配置信息的库。它可以处理多种格式的配置。它拥有以下功能及特性:
- 设置默认值
- 从JSON、TOML、YAML、HCL和Java properties文件中读取配置数据
- 可以监视配置文件的变动、重新读取配置文件
- 从环境变量中读取配置数据
- 从远端配置系统中读取数据,并监视它们(比如etcd、Consul)
- 从命令参数中读物配置
- 从buffer(缓冲区)中读取
- key不区分大小写
为什么要使用Viper
使用 Viper,你不用担心配置文件格式,能满足不同的对配置文件的使用要求,专注于构建项目
- 加载并解析JSON、TOML、YAML、HCL 或 Java properties 格式的配置文件
- 可以为各种配置项设置默认值
- 可以在命令行中指定配置项来覆盖配置值
- 提供了别名系统,可以不破坏现有代码来实现参数重命名
- 可以很容易地分辨出用户提供的命令行参数或配置文件与默认相同的区别
Viper 读取配置信息的优先级顺序,从高到低,如下:
- 显式调用Set
- 命令行参数
- 环境变量
- 配置文件
- key/value 键值存储
- 默认值
Viper 的配置项的 key 不区分大小写。
设置默认值
默认值不是必须的,如果配置文件、环境变量、远程配置系统、命令行参数、Set函数都没有指定时,默认值将起作用。
例子:
viper.SetDefault("ConfigDir","conf")
读取配置文件
Viper 支持从 JSON、TOML、YAML、HCL和 Java properties 文件中读取数据且可以搜索多个路径,但目前单个 Viper 实例仅支持单个配置文件。Viper 默认不搜索任何路径。以下是如何使用 Viper 搜索和读取配置文件的示例。路径不是必需的,但最好至少应提供一个路径,以便找到一个配置文件。
viper.SetConfigName("config") //配置文件的名称(不带扩展名)
viper.AddConfigPath("/etc/appname/") //查找配置文件的路径
viper.AddConfigPath("$HOME/.appname") //添加可以多次调用的路径
viper.ReadInConfig() //查找并读取配置文件
监视配置文件,重新读取配置数据
Viper 支持在运行时让应用程序实时读取配置文件。
需要重新启动服务器以使配置生效的日子已经一去不复返了,由 Viper 驱动的应用程序可以在运行时读取配置文件的更新,而不会错过任何一个数据变动。
只需要调用 Viper 实例的 WatchConfig 即可,确保在调用 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")
//将一些配置加入程序的方法
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") //将返回"steve"
Set调用(设置覆盖)
这些可以来自命令行标志,也可以来自你自己的应用程序逻辑。
viper.Set("Verbose", true)
viper.Set("LogFile", LogFile)
注册并使用别名
别名允许多个键引用单个值。
viper.RegisterAlias("loud", "Verbose")
viper.Set("verbose", true)
viper.Set("loud", true) //这两句设置的都是同一个值
viper.GetBool("loud") //返回true
viper.GetBool("verbose") //返回true
从环境变量中读取
Viper 完全支持环境变量,你可以自动读取与键匹配的任何 ENV 变量(带或不带前缀)或将环境变量显式绑定到键。后者为 ENV别名提供了一种简单有效的机制。有五个和环境变量有关的方法:
- AutomaticEnv()
- BindEnv(string...) : error
- SetEnvPrefix(string)
- SetEnvKeyReplacer(string...) *strings.Replacer
- AllowEmptyEnvVar(bool)
注意:使用ENV变量时,重要的是要认识到Viper将ENV变量视为区分大小写。
Viper 提供了一种机制来确保 ENV 变量是唯一的。通过使用 SetEnvPrefix,在从环境变量读取时会添加设置的前缀。BindEnv和 AutomaticEnv 都将使用此前缀。
BindEnv 需要一个或两个参数。第一个参数是键名,第二个是环境变量的名称。环境变量的名称区分大小写。如果未提供 ENV 变量名,则 Viper 将自动假定该键名称与 ENV 变量名称匹配,并且 ENV 变量全部为大写。当你明确提供 ENV 变量名称时,它不会自动添加前缀。
使用 ENV 变量时要注意,当关联后,每次访问时都会读取该 ENV 值。Viper 在 BindEnv 调用时不读取 ENV 值。
AutomaticEnv 与 SetEnvPrefix 结合将会特别有用。当 AutomaticEnv 被调用时,任何 viper.Get 请求都会去获取环境变量。环境变量名为 SetEnvPrefix 设置的前缀,加上对应名称的大写。
SetEnvKeyReplacer 允许你使用一个 strings.Replacer 对象来将配置名重写为 Env 名。如果你想在 Get() 中使用包含-的配置名 ,但希望对应的环境变量名包含_分隔符,就可以使用该方法。使用它的一个例子可以在项目中 viper_test.go 文件里找到。
默认情况下,空环境变量被视为未设置,并将回退到下一个配置源。要将空环境变量视为 set,请使用 AllowEmptyEnv 方法。
SetEnvPrefix("spf") //将自动转换为大写
BindEnv("id")
os.Setenv("SPF_ID", "13") //通常通过系统环境变量来设置
id := Get("id") //13
从命令行标志读取
Viper 能够通过该 BindPFlag() 方法支持Cobra库中使用的 Pflags。
绑定标志时,它将设置标志定义的默认值以及用户在命令行上提供的值。Viper 足够聪明,可以区分默认值和标志值,即使它们是相同的,也会正确应用覆盖。
和 BindEnv 一样,当绑定方法被调用时,该值没有被获取,而是在被访问时获取。这意味着应该尽早进行绑定,甚至是在 init()函数中绑定。
利用 BindPFlag() 方法可以绑定单个flag。
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") //通过viper从pflag中获取值
使用 pflag 并不影响其他库使用标准库中的 flag。pflag 包可以通过导入这些标志来处理为 flag 包定义的标志。这是通过调用 pflag 包中的 AddGoFlagSet() 方法实现的。
package main
import (
"flag"
"github.com/spf13/pflag"
)
func main() {
//使用标准库 flag包
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接口
如果你不想使用 pflag,Viper 提供了两个接口来实现绑定其他的 flag 系统。
使用 FlagValue 接口代表单个 flag。下面是实现了该接口的简单的例子:
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" }
一旦你实现了该接口,就可以绑定它:
viper.BindFlagValue("my-flag-name", myFlag{})
使用 FlagValueSet 接口代表一组 flag。下面是实现了该接口的简单的例子:
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/remot 包:import "github.com/spf13/viper/remote"
Viper 可以从例如etcd、Consul的远程 Key/Value 存储系统的一个路径上,读取一个配置字符串(JSON, TOML, YAML或HCL格式)。
这些值优先于默认值,但会被从磁盘文件、命令行flag、环境变量的配置所覆盖。
Viper 使用 crypt 来从 K/V 存储系统里读取配置,这意味着你可以加密储存你的配置信息,并且可以自动解密配置信息。加密是可选的。
您可以将远程配置与本地配置结合使用,也可以独立使用。
crypt有一个命令行工具可以帮助你存储配置信息到K/V存储系统,crypt默认使用 http://127.0.0.1:4001上的etcd。
go get github.com/xordataexchange/crypt/bin/crypt
crypt set -plaintext /config/hugo.json /Users/hugo/settings/config.json
确认你的值被设置:crypt get -plaintext /config/hugo.json
有关crypt如何设置加密值或如何使用 Consul 的示例,请参阅文档。下面以 etcd举例:
远程Key/Value存储示例 - 未加密
viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001","/config/hugo.json")
//因为不知道格式,所以需要指定,支持的扩展名有“json”,“toml”,“yaml”,“yml”,“properties”,“props”,“prop”
viper.SetConfigType("json")
err := viper.ReadRemoteConfig()
远程Key/Value存储示例 - 加密
viper.AddSecureRemoteProvider("etcd","http://127.0.0.1:4001","/config/hugo.json",
"/etc/secrets/mykeyring.gpg")
//因为不知道格式,所以需要指定,支持的扩展名有“json”,“toml”,“yaml”,“yml”,“properties”,“props”,“prop”
viper.SetConfigType("json")
err := viper.ReadRemoteConfig()
监视etcd的变化 - 未加密
//可以创建一个新的viper实例
var runtime_viper = viper.New()
runtime_viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001", "/config/hugo.yml")
//因为不知道格式,所以需要指定,支持的扩展名有“json”,“toml”,“yaml”,“yml”,“properties”,“props”,“prop”
runtime_viper.SetConfigType("yaml")
//从远程读取配置
err := runtime_viper.ReadRemoteConfig()
//解析配置到runtime_conf中
runtime_viper.Unmarshal(&runtime_conf)
//通过一个goroutine监控远程的配置变化
go func(){
for {
//每次请求后睡眠五秒
time.Sleep(time.Second * 5)
//目前,仅测试了etcd支持
err := runtime_viper.WatchRemoteConfig()
if err != nil {
log.Errorf("unable to read remote config: %v", err)
continue
}
//解析新的配置到一个结构体变量中,你也可以使用channel实现一个信号通知的方式
runtime_viper.Unmarshal(&runtime_conf)
}
}()
获取值
- Get(key string) : interface{}
- GetBool(key string) : bool
- GetFloat64(key string) : float64
- GetInt(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") //Setting & Getting 不区分大小写
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") //(returns "127.0.0.1")
这遵守前面确立的优先规则; 会搜索路径中所有配置,直到找到为止。
例如,上面的文件,datastore.metric.host和 datastore.metric.port都已经定义(并且可能被覆盖)。如果另外 datastore.metric.protocol的默认值,Viper也会找到它。
但是,如果datastore.metric值被覆盖(通过标志,环境变量,Set方法,...),则所有datastore.metric的子键将会未定义,它们被优先级更高的配置值所“遮蔽”。
最后,如果存在相匹配的嵌套键,则其值将被返回。例如:
{
"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"
提取子树配置
可以从viper中提取子树。例如, viper配置为:
app:
cache1:
max-items: 100
item-size: 64
cache2:
max-items: 200
item-size: 80
执行后:subv := viper.Sub("app.cache1")
subv 就代表:
max-items: 100
item-size: 64
假如我们有如下函数:
func NewCache(cfg *Viper) *Cache {...}
它的功能是根据配置信息创建缓存。现在很容易分别创建这两个缓存:
cfg1 := viper.Sub("app.cache1")
cache1 := NewCache(cfg1)
cfg2 := viper.Sub("app.cache2")
cache2 := NewCache(cfg2)
解析配置
您还可以选择将所有或特定值解析到 struct、map等。
有两个方法可以做到这一点:
- 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 := Unmarshal(&C)
if err != nil {
t.Fatalf("unable to decode into struct, %v", err)
}
转换为字符串
你可能需要将 viper 中保存的所有设置变为字符串,而不是将其写入文件。你可以使用 AllSettings() 方法。
import (
yaml "gopkg.in/yaml.v2"
)
func yamlStringSettings() string {
c := viper.AllSettings()
bs, err := yaml.Marshal(c)
if err != nil {
t.Fatalf("unable to marshal config to YAML: %v", err)
}
return string(bs)
}
Viper or Vipers?
Viper 随时可以使用。开始使用 Viper 无需配置或初始化。由于大多数应用程序都希望使用单个存储中心进行配置,因此 Viper 包提供了此功能。它类似于一个单例模式。
使用多个viper实例
你还可以创建多个不同的 viper 实例以供你的应用程序使用。每个实例都有自己独立的设置和配置值。每个实例可以从不同的配置文件,K/V存储系统等读取。viper包支持的所有函数也都有对应的 viper 实例方法。
x := viper.New()
y := viper.New()
x.SetDefault("ContentDir", "content")
y.SetDefault("ContentDir", "foobar")
当使用多个viper实例时,用户需要自己管理每个实例。