项目中使用到了Viper配置环境,本文主要想对Viper的用法进行整理,对Viper仓库的README文件进行了翻译用于记录学习。
一、什么是Viper
Viper是一个用于go语言项目配置的库,它可以简化项目的配置过程,具有灵活且丰富的特性。
二、什么使用Viper
1.支持多种方式设置
- 默认值
- 通过读取
JSON、TOML、YAML、HCL、envfile
和Java
属性配置文件 - 实时监听并重新读取配置文件
- 从环境变量中读取
- 从远程配置系统(
etcd
或Consul
)读取,并监听修改 - 从命令行的
flag
中读取 - 从
buffer
中读取
2.丰富的特性
- 可以从
JSON
、TOML
、YAML
、HCL
、INI
、envfile
和Java properties
格式中进行查找、加载以及反序列化 - 提供一种机制来为不同的配置选项设置默认值。
- 提供一种机制,用于为通过命令行标志指定的选项设置替代值。
- 提供别名系统,以便在不破坏现有代码的情况下轻松重命名参数。
- 轻松区分用户何时提供了与默认相同的命令行或配置文件。
三、Viper安装以及使用
1.安装
js
$ go get github.com/spf13/viper
2.使用
go
package main
import (
"github.com/spf13/viper"
)
func main() {
//...具体配置
}
四、Viper配置项
1.配置项优先级
- 显式调用
Set
设置值 - 命令行参数(flag)
- 环境变量
- 配置文件
- key/value存储
- 默认值
1.设置默认值:SetDefault
go
//配置项: ContentDir 默认值: content
viper.SetDefault("ContentDir", "content")
2.读取配置文件:
go
viper.SetConfigName("config") // 配置文件名称(无扩展名)
viper.SetConfigFile("./config.yaml") // 指定配置文件路径
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))
}
编写配置文件
go
// 将当前 viper 配置写入预定义的路径(如果存在)。如果没有预定义的路径,将报错。如果配置文件已经存在则将覆盖当前配置文件。
viper.WriteConfig()
// 和WriteConfig类似只是不会覆盖
viper.SafeWriteConfig()
//
viper.WriteConfigAs("/path/to/my/.config")
viper.SafeWriteConfigAs("/path/to/my/.config")
viper.SafeWriteConfigAs("/path/to/my/.other_config")
查看和重新读取配置文件
Viper 支持应用程序在运行时实时读取配置文件。 可以通过配置使得Viper监听配置的变动,也可以选择在变动之后触发对应的回调函数
go
//监听变化
viper.WatchConfig()
// 变动之后的回调
viper.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("Config file changed:", e.Name)
})
从io.Reader中读取
Viper 预定义了许多配置源,例如文件、环境 变量、标志和远程 K/V 存储,除此此外,也可以自己配置源并将其提供给 Viper。
go
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"
覆盖设置
go
viper.Set("Verbose", true)
viper.Set("LogFile", LogFile)
viper.Set("host.port", 5899) // set subset
注册和使用别名
别名允许多个键引用单个值
go
viper.RegisterAlias("loud", "Verbose")
viper.Set("verbose", true) // 结果和下一行相同
viper.Set("loud", true) // 结果和前一行相同
viper.GetBool("loud") // true
viper.GetBool("verbose") // true
使用环境变量
Viper完全支持环境变量,它使得12 factor applications
应用程序做到开箱即用,通过如下5个函数可以提供对环境变量的支持:
AutomaticEnv()
BindEnv(string...) : error
SetEnvPrefix(string)
SetEnvKeyReplacer(string...) *strings.Replacer
AllowEmptyEnv(bool)
在使用环境变量时,需要注意Viper对环境变量是区分大小写的 Viper通过一种机制去确保ENV变量是唯一的,SetEnvPrefix
可以指定Viper在读取环境变量时的前缀。
BindEnv
需要传入一个或者多个参数,第一个参数是键名,其余参数是该键名所对应的花环境变量,如果其有一个以上,会按照指定的优先顺序处理。如果没有提供ENV变量名,则会去匹配规则为:前缀+ "_" +键名全部大写。如果在第二个参数中显式的提供了ENV变量,则不会去自动的添加前缀例如,如果第二个参数是"id",Viper将查找环境变量"ID"。 在使用ENV变量的时候一个重要的点是,ENV变量的值会在其每次被访问时读取,Viper在调用BindEnv时不会固定ENV变量的值。
AutomaticEnv
是一个很强大的助手,尤其是与SetEnvPrefix
结合使用时,在任何时候当viper.Get
请求发出时,Viper会随时对环境变量进行检查。它遵循如下的规则:如果设置了EnvPrefix
,会检查一个环境变量的名称是否匹配大写的键和EnvPrefix
所设置的前缀。
SetEnvKeyReplacer
允许你使用strings.Replacer
对象去重写ENV的键,这在你希望于Call()
调用过程中使用分隔符-
,但是在环境变量中使用_
,这将非常有用。
与此同时,你也可以使用EnvKeyReplacer
以及NewWithOptions
工厂函数,不同于SetEnvKeyReplacer
,它能接收一个StringReplacer
接口允许你去自定义字符串的替换逻辑。
使用Env的示例如下:
go
SetEnvPrefix("spf") // 会被自动变为大小写
BindEnv("id")
os.Setenv("SPF_ID", "13") // 通常在程序之外处理完成
id := Get("id") // 13
使用Flags
Viper有绑定flags的能力,具体的,Viper支持如Cobra 库中使用的pflgs
,和BindEnv
类似,其值不是在绑定方法被调用的时候被设置的,而是在其被访问的时候。这意味着你可以尽早的对其进行绑定,即使在init()
中也能起作用。
如果需要绑定单个flags,可以使用BindPFlag
方法,实例如下:
go
serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))
也可以绑定一组现有的pflags (pflag.FlagSet)
:
go
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,并不影响标准库中flag包的使用,flag包可以通过使用AddGoFlagSet()
导入这些 flags 来处理其定义的flags,比如:
go
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接口
如果不适用Pflags,Viper提供了两个接口绑定其它的flag系统,FlagValue
代表单个的flag,以下是一个如何实现这个接口的简单例子:
go
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对其进行绑定
go
viper.BindFlagValue("my-flag-name", myFlag{})
FlagValueSet
表示一组flags,以下是一个如何实现这个接口的简单例子:
go
type myFlagSet struct {
flags []myFlag
}
func (f myFlagSet) VisitAll(fn func(FlagValue)) {
for _, flag := range flags {
fn(flag)
}
}
一旦你的flag实现了这样的接口,就可以很方便的通过Viper对其进行绑定
go
fSet := myFlagSet{
flags: []myFlag{myFlag{}, myFlag{}},
}
viper.BindFlagValues("my-flags", fSet)
远程Key/Value存储支持
若要在Viper中启动远程支持,需要匿名导入viper/remote
包
go
import _ "github.com/spf13/viper/remote"
Viper将会从类似(ectd
或者Consul
)的Key/Value存储中路径中检索到配置字符串如(as JSON, TOML, YAML, HCL or envfile
) 这些值的优先级高于默认值,但是会被从磁盘、flags
和环境变量中检索到的 配置值所覆盖。
Viper支持多主机,如果要使用该种配置,需要使用;
进行分隔离,比如: http://127.0.0.1:4001;http://127.0.0.1:4002
Viper使用crypt从K/V存储中检索配置,这意味着你可以通过加密的方式存储配置的值,并且在拥有gpg密钥的情况下自动解密,加密功能是可选的。
你可以同时使用远程和本地配置,也可以单独使用远程配置。
crypt
有一个命令行助手用来将配置存到K/V存储中,crypt
默认使用位于http://127.0.0.1:4001 的 etcd
go
$ go get github.com/sagikazarmark/crypt/bin/crypt
$ crypt set -plaintext /config/hugo.json /Users/hugo/settings/config.json
确认你的值已经设置:
go
$ crypt get -plaintext /config/hugo.json
查阅crypt
文档的例子可以了解如何进行加密以及如何使用Consul
远程 K/V存储示例 - 未加密
etcd
go
viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001","/config/hugo.json")
viper.SetConfigType("json") // 因为字节流中没有扩展名,需要设置,支持的扩展名为:"json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv"
err := viper.ReadRemoteConfig()
etcd3
go
viper.AddRemoteProvider("etcd3", "http://127.0.0.1:4001","/config/hugo.json")
viper.SetConfigType("json") // 因为字节流中没有扩展名,需要设置,支持的扩展名为:"json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv"
err := viper.ReadRemoteConfig()
Consul
你需要在Consul key/value存储中设置一个key,包含所需配置的JSON值,比如下面所示的MY_CONSUL_KEY
go
{
"port": 8080,
"hostname": "myhostname.com"
}
go
viper.AddRemoteProvider("consul", "localhost:8500", "MY_CONSUL_KEY")
viper.SetConfigType("json") // 需要显式设置成格式'json'
err := viper.ReadRemoteConfig()
fmt.Println(viper.Get("port")) // 8080
fmt.Println(viper.Get("hostname")) // myhostname.com
Firestore
go
viper.AddRemoteProvider("firestore", "google-cloud-project-id", "collection/document")
viper.SetConfigType("json") // 配置格式: "json", "toml", "yaml", "yml"
err := viper.ReadRemoteConfig()
当然,你也可以使用SecureRemoteProvider
NATS
go
viper.AddRemoteProvider("nats", "nats://127.0.0.1:4222", "myapp.config")
viper.SetConfigType("json")
err := viper.ReadRemoteConfig()
远程Key/Value存储示例-加密
go
viper.AddSecureRemoteProvider("etcd","http://127.0.0.1:4001","/config/hugo.json","/etc/secrets/mykeyring.gpg")
viper.SetConfigType("json") // 因为字节流中没有扩展名,需要设置,支持的扩展名为:"json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv"
err := viper.ReadRemoteConfig()
监听etcd中的更改-未加密
go
// 同样的,你可以创建一个新的viper实例
var runtime_viper = viper.New()
runtime_viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001", "/config/hugo.yml")
runtime_viper.SetConfigType("yaml") // 因为字节流中没有扩展名,需要设置,支持的扩展名为: "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv"
// 第一次从远程配置中读取
err := runtime_viper.ReadRemoteConfig()
// 反序列化配置
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)
}
}()
从Viper中获取值
在Viper中,根据值的类型不同,有很多方法去获取,有以下获取值的的方法:
Get(key string) : any
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]any
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]any
有一件重要的事需要被认识到: 当值没有被找到时,每一个函数都会返回一个零值,如果需要检查一个给定的key是否存在,Viper提供了IsSet()
方法。
例子:
go
viper.GetString("logfile") // case-insensitive Setting & Getting
if viper.GetBool("verbose") {
fmt.Println("verbose enabled")
}
访问嵌套的键
访问器方法也接受深度嵌套键的格式化路径,例如加载如下的JSON文件
go
{
"host": {
"address": "localhost",
"port": 5799
},
"datastore": {
"metric": {
"host": "127.0.0.1",
"port": 3099
},
"warehouse": {
"host": "198.0.0.1",
"port": 2112
}
}
}
Viper可以通过.
分隔符去访问嵌套
go
GetString("datastore.metric.host") // (返回 "127.0.0.1")
这遵守上述的优先规则,对于路径的搜索将会依次在配置注册表中寻找,直到找到为止 ,例如有这样一个配置文件,datastore.metric.host
和 datastore.metric.port
均已经被定义(并且可能会被覆盖),另外 datastore.metric.protocol
也在默认值中被定义, Viper 也会找到他。
然而,如果datastore.metric
被立即值覆盖(通过flag,环境变量,Set()方法等), 那么datastore.metric
的所有子键都将变为未定义状态,它们被高优先级配置级别遮蔽了。
Viper可以通过在路径中使用数字去访问数组的索引值,例如:
go
{
"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
最后,如果存在一个和分隔键路径匹配的,其值将会被返回,例如:
go
{
"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"
提取子树
在开发可重用模块时,通常会提取配置的子集并将其传递给模块是很有用的。这样一来,可以多次实例化该模块,使用不同的配置。 例如,一个应用程序可能会为不同的目的使用多个不同的缓存存储:
go
cache:
cache1:
max-items: 100
item-size: 64
cache2:
max-items: 200
item-size: 80
我们可以将缓存名称传递给一个模块(例如 NewCache("cache1")),但这将需要奇怪的串联来访问配置键,并且与全局配置更少分离。
因此,我们不这样做,而是将表示配置子集的 Viper 实例传递给构造函数:
go
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)
注意:始终检查 Sub 的返回值。如果找不到键,它将返回 nil
。 在内部,NewCache
函数可以直接访问 max-items
和 item-size
键
go
func NewCache(v *Viper) *Cache {
return &Cache{
MaxItems: v.GetInt("max-items"),
ItemSize: v.GetInt("item-size"),
}
}
由此产生的代码易于测试,因为它与主配置结构解耦,并且更容易重用(出于同样的原因)。
反序列化
ChatGPT
你还可以选择将所有或特定值解码到结构体、映射等。
有两种方法可以做到这一点:
Unmarshal(rawVal any) : error
UnmarshalKey(key string, rawVal any) : error
例如:
go
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)
}
如果你想解码包含点的配置(默认键分隔符)的配置,你需要更改分隔符:
go
v := viper.NewWithOptions(viper.KeyDelimiter("::"))
v.SetDefault("chart::values", map[string]any{
"ingress": map[string]any{
"annotations": map[string]any{
"traefik.frontend.rule.type": "PathPrefix",
"traefik.ingress.kubernetes.io/ssl-redirect": "true",
},
},
})
type config struct {
Chart struct{
Values map[string]any
}
}
var C config
v.Unmarshal(&C)
Viper还支持将值解码到嵌套结构中:
go
/*
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/go-viper/mapstructure
库,该库默认使用 mapstructure
标签。
解码自定义格式
解码自定义格式 Viper经常被要求添加更多值格式和解码器的功能。例如,将字符(点、逗号、分号等)分隔的字符串解析成切片。
Viper已经支持了这一功能,使用 mapstructure 解码钩子。
了解更多详情,请参阅文章。
序列化成字符串
你可能需要将Viper中保存的所有设置序列化为字符串,而不是写入文件。你可以使用你喜欢的格式的序列化器和AllSettings()
返回的配置。
go
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实例,并在必要时传递该实例。
全局实例可能在未来被弃用。有关更多详情,请参阅[#1855].(github.com/spf13/viper...)
使用多个Viper
您还可以创建许多不同的Viper实例来用于您的应用程序。每个实例将具有其自己独特的配置和值。每个实例都可以从不同的配置文件、键值存储等读取。Viper包支持的所有函数都被作为Viper实例的方法进行了镜像。
示例:
go
x := viper.New()
y := viper.New()
x.SetDefault("ContentDir", "content")
y.SetDefault("ContentDir", "foobar")
在使用多个Viper实例时,用户需要自行跟踪这些不同的实例。