应用程序通常都需要管理配置,这些配置可以来自命令行参数、环境变量、配置文件等等。Viper 库 是 Go 中一个经典配置管理库,为 Go 应用程序提供完整的配置解决方案。许多开源 Go 项目都使用了 Viper,例如 Hugo
、Cilium
等等。这篇文章会对 Viper 库的使用做一个基本介绍。
Viper 简介
Viper 为 Go 应用程序(包括 12-Factor apps)提供完整的配置解决方案,它可以满足应用程序各种配置需求,包括:
- 默认值设置
- 从 JSON、TOML、YAML、HCL、envfile、Java properties 格式的配置文件中读取配置
- 实时监控并重读配置文件(可选)
- 从环境变量中读取配置
- 从远端配置系统(etcd、Consul 等)读取配置,并监控变化
- 从命令行 flags 中读取配置
- 从 buffer 中读取配置
- 显式配置值
在现代应用程序中,不应该关心配置文件的具体格式,只需要专注于构建软件本身即可。Viper 可以帮助你实现上述目标:
- 查找、加载、反序列化配置文件:支持 JSON、TOML 等多种配置文件格式
- 可以为不同的配置选项提供默认值
- 支持通过命令行 flags 来覆盖配置选项的值
- 提供了别名系统,在不破坏现有代码的前提下重命名参数
- 当用户使用命令行 flags 或配置文件提供的配置值和默认值相同时,可以轻易区分出这两者
Viper 按照如下优先级进行配置解析,优先级从高到低排列:
- 显式调用
Set
设置值 - flag
- env
- config
- key/value 存储
- default
需要注意,Viper 配置的 key 是大小写不敏感的。
把值存入 Viper 中
设置默认值
一个优秀的配置系统都会支持默认值,当用户没有显式配置某个配置项时,默认值就有用了。
Viper 通过如下方式设置默认值:
1 | viper.SetDefault("ContentDir", "content") |
从配置文件中读取
Viper 本身也需少量设置,这样才能知道去哪里查找配置文件。Viper 支持多种格式的配置文件,可以从多个路径中查找配置文件,但是目前单个 Viper 实例只支持单个配置文件。Viper 没有提供任何默认的 配置文件搜索路径
,而是由应用程序自己决定。
如下是一个使用 Viper 查找、读取配置文件的示例:
1 | viper.SetConfigName("config") // name of config file (without extension) |
如下特别处理了配置文件查找失败的错误:
1 | if err := viper.ReadInConfig(); err != nil { |
写入配置文件
有时我们想保存在运行时所做的所有修改。Viper 提供了一系列接口来实现该目的:
- WriteConfig:将当前 viper 配置保存到预定义路径中。如果没有预定义路径则出错。会覆盖该路径下已经存在的配置文件
- SafeWriteConfig:将当前 viper 配置保存到预定义路径中。如果没有预定义路径则出错。不会覆盖该路径下已经存在的配置文件
- WriteConfigAs:将当前 viper 配置保存到指定路径中,会覆盖该路径下已经存在的配置文件
- SafeWriteConfigAs:将当前 viper 配置保存到指定路径中,不会覆盖该路径下已经存在的配置文件
示例如下:
1 | viper.WriteConfig() // writes current config to predefined path set by 'viper.AddConfigPath()' and 'viper.SetConfigName' |
监控并重新读取配置文件
Viper 支持让应用程序在运行时实时读取配置文件。重启服务器以让配置重新生效的日志过去了,viper 可以让应用程序在运行时读取到配置文件的更新。
可以让 viper 实例监听配置更新,并且指定一个回调函数,每次配置更新时都运行该函数:
1 | viper.OnConfigChange(func(e fsnotify.Event) { |
**在调用 WatchConfig()
之前需要确保所有的配置路径都已经添加了。
从 io.Reader 中读取配置
Viper 预定义了许多配置源,例如文件、环境变量、flags、远端 KV 存储,但是不局限于这些配置源。你可以实现自己的配置源,并提供给 viper:
1 | viper.SetConfigType("yaml") // or viper.SetConfigType("YAML") |
覆盖设置
这些覆盖值可以来自命令行 flag,也可能来源于自己的程序逻辑:
1 | viper.Set("Verbose", true) |
注册并使用别名
别名可以让某个值通过多个 key 引用:
1 | viper.RegisterAlias("loud", "Verbose") |
使用环境变量
Viper 完全支持环境变量,从而天然满足 12 factor applications
。Viper 提供了 5 个方法来支持环境变量:
AutomaticEnv()
BindEnv(string...) : error
SetEnvPrefix(string)
SetEnvKeyReplacer(string...) *strings.Replacer
AllowEmptyEnv(bool)
当使用环境变量时,需要注意 Viper 对待环境变量是大小写敏感的。
Viper 提供了一种保证变量唯一的机制。SetEnvPrefix
可以告诉 viper 读取变量时都使用一个前缀。这个前缀对 BindEnv
和 AutomaticEnv
方法都有效。
BindEnv
接受一个或多个参数。第一个参数是 key 的名称,之后的参数则是与该 key 绑定的环境变量的名称。如果提供了多个,则优先级按照参数顺序进行匹配。如果没有提供,那么 Viper 自动假定环境变量使用如下格式:prefix
+ _
+ 全大写形式的 key
。注意,如果显示提供了环境变量名称,不会自动添加 prefix。例如如果第二个参数是 id
,那么 Viper 会查找名称为 ID
的环境变量。
使用环境变量还有一点需要注意,每次访问值的时候都会重新读取读取,而不是在调用 BindEnv
时固定值。
当使用 AutomaticEnv
时,每次调用 viper.Get
时 Viper 都会检查环境变量:所检查的环境变量名称为 prefix
+ 全大写形式的 key
。
SetEnvKeyReplacer
允许你使用一个 strings.Replacer
对象来重写 Env 的 key。例如当你使用 Get()
时希望使用 -
,但环境变量却使用 _
作为分隔符,此时该方法就可以发挥作用了。
或者你可以在 NewWithOptions
中使用 EnvKeyReplacer
选项,和 SetEnvKeyReplacer
不同,它可以接受一个 StringReplacer
接口,从而允许你自定义字符串替换逻辑。
默认空的环境变量被认为是 unset
的,因此会继续尝试下一个配置源。如果需要将空的环境变量认为是 set
的,可以使用 SetEnvKeyReplacer
方法。
如下是使用 Env 的示例:
1 | SetEnvPrefix("spf") // will be uppercased automatically |
使用 Flags
Viper 支持和 flags 绑定,具体来说,Viper 支持 Cobra 库中使用的 Pflags。
和 BindEnv
类似,并不是 bind 方法调用时设置值,而是在访问时设置值的。所以可以任意早(甚至在 init()
中` 地调用 bind 方法。例如:
1 | serverCmd.Flags().Int("port", 1138, "Port to run Application server on") |
也可以绑定到一个已经存在的 pflags set 上:
1 | pflag.Int("flagname", 1234, "help message for flagname") |
Viper 使用 pflag 包并不妨碍其他包使用标准库中的 flag 包。通过导入这些 flag,pflag 包可以处理那些由 flag 包定义的 flag。该操作由 pflag 包提供的 AddGoFlagSet()
实现:
1 | package main |
如果你不使用 Pflags
,Viper 提供了两个接口来绑定其他 flag 包:
FlagValue
代表单个 flag,一旦实现该接口,可以让 Viper 绑定它:
1 | type myFlag struct {} |
FlagValueSet
代表一组 flags,实现了该接口后,同样可以让 Viper 绑定它
1 | type myFlagSet struct { |
远端 Key/Value 存储支持
为了在 Viper 中开启远端支持,需要匿名导入 viper/remote
包:
1 | import _ "github.com/spf13/viper/remote" |
Viper 可以从 Key/Value(例如 etcd、Cousul 等)的路径检索出配置字符串(例如 JSON、TOML 等)。Viper 支持多个主机,使用 ;
分隔:
1 | http://127.0.0.1:4001;http://127.0.0.1:4002. |
Viper 使用 crypt
来从 K/V 存储中检索配置,这意味着你如果有正确的 gpg
秘钥,你可以将配置值加密存储并自动解密。加密是可选的。crypt
提供了命令行助手,可以直接将配置保存到 K/V 存储中,crypt
默认连接到 etcd(http://127.0.0.1:4001)。
1 | $ go get github.com/bketelsen/crypt/bin/crypt |
如下是使用 K/V 存储的示例,首先是未加密的示例:
etcd:
1 | viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001","/config/hugo.json") |
etcd3:
1 | viper.AddRemoteProvider("etcd3", "http://127.0.0.1:4001","/config/hugo.json") |
Consul:
首先需要在 Consul
中设置一个 Key,其 Value 是你希望保存的配置,例如创建一个 MY_CONSUL_KEY
,其 value 如下:
1 | { |
1 | viper.AddRemoteProvider("consul", "localhost:8500", "MY_CONSUL_KEY") |
Firestore:
1 | viper.AddRemoteProvider("firestore", "google-cloud-project-id", "collection/document") |
NATS:
1 | viper.AddRemoteProvider("nats", "nats://127.0.0.1:4222", "myapp.config") |
当然也可以使用 SecureRemoteProvider
。如下是使用加密形式的 K/V 存储的示例:
1 | viper.AddSecureRemoteProvider("etcd","http://127.0.0.1:4001","/config/hugo.json","/etc/secrets/mykeyring.gpg") |
如下展示了如何 watch etcd 中的变化(未加密):
1 | // alternatively, you can create a new viper instance. |
从 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 是否存在,提供了 IsSet()
方法。另外,即使配置存在,但是将其值转换为指定的类型失败时,也会返回 零值
。
1 | viper.GetString("logfile") // case-insensitive Setting & Getting |
访问嵌套的 key
以上访问方法支持格式化的路径来检索深层嵌套的 key。例如如果存在如下 json 配置文件:
1 | { |
可以使用如下方法进行嵌套 key 的访问:使用 .
作为 key 路径的分隔符:
1 | GetString("datastore.metric.host") // (returns "127.0.0.1") |
这个查找过程仍然符合上面介绍的配置源搜索优先级。例如对于该配置文件,datastore.metric.host
、datastore.metric.port
都被正常定义了。如果默认值中有 datastore.metric.protocol
,Viper 也可以找到该配置。但是如果 datastore.metric
被某个值直接覆盖了(例如通过 Set()、环境变量等等),那么 datastore.metric
下的所有子 key 都是未定义状态:它们被优先级更高的配置源给 shadowed
了。
Viper 可以在 path 中使用索引访问数组中的元素:
1 | { |
1 | GetInt("host.ports.1") // returns 6029 |
最后,如果存在某个 key 匹配该分隔路径,那么会直接返回该 key 的值:
1 | { |
1 | GetString("datastore.metric.host") // returns "0.0.0.0" |
提取子树
当开发一些可重用的模块时,通常需要提取配置中的某个子集并将其传递给模块。通过这种方式,模块可以多次实例化,每次使用不同的配置。
例如某个应用程序可以根据不同的目的使用不同的 cache 配置:
1 | cache: |
我们可以给模块直接传递 cache 的名称,例如 NewCache("cache1")
,但是这种方式在访问 key 时需要进行额外的连接,而且和全局配置也无法隔开。更好的方法是直接传递给 Viper 某个配置的子集:
1 | cache1Config := viper.Sub("cache.cache1") |
在 NewCache 函数内部可以直接访问 max-items
、item-size
keys:
1 | func NewCache(v *Viper) *Cache { |
这样我们的代码更加容易测试,也更易于使用,因为它和全局配置结构解耦了。
反序列化
你也可以将所有或特定的值反序列化到 struct、map 等。Viper 提供了两个方法:
- Unmarshal(rawVal any) : error
- UnmarshalKey(key string, rawVal any) : error
例如:
1 | type config struct { |
如果需要反序列化的配置本身就包含 .
(默认的 key 分隔符),你可以修改分隔符:
1 | v := viper.NewWithOptions(viper.KeyDelimiter("::")) |
Viper 也支持反序列化到嵌套的结构体中:
1 | /* |
Viper 内部是使用 mapstructure 来反序列化值的,所以可以看到该库所使用的 mapstructure
tag。
自定义格式解码
Viper 通过使用 mapstructure decode hooks
来支持自定义格式解码,具体可以参考Decoding custom formats with Viper。
序列化成字符串
有时你需要将 viper 中保存的配置序列化成字符串,而不是写入文件中。你可以使用你喜欢格式的序列化器将 AllSettings()
返回的配置进行序列化:
1 | import ( |
Viper 还是 Vipers
viper 包开箱即用,不需要配置或初始化就能直接使用 viper。绝大多数应用程序都希望有一个集中的仓库来管理它们的配置信息,viper 包就是这样做的,这类似于单例模式。以上的实例也都是以 单例风格
演示如何使用 viper。
你可以在应用程序中创建多个 viper 实例,每个 viper 实例都有自己唯一的一组配置和值。每个 viper 实例可以从不同的配置文件、K/V 存储等读取配置。Viper 包提供的所有函数都有对应的方法(操作在某个 viper 实例上)。
例如:
1 | x := viper.New() |
当使用多个 viper 实例时,由用户来跟踪不同的 viper 实例。
最后需要注意,viper 不是并发安全的。对某个 viper 的实例的并发读写,需要自己做同步操作。
简单示例
当前目录下存在 test.yaml
配置文件,内容如下:
1 | languages: |
如下 go 程序用于对该配置文件进行读取并反序列化为配置结构体:
1 | package main |