【本文章作为Viper代码研究的学习笔记,翻译自官方文档,内容排版有所出入】
Viper - Go配置管理解决方案
Viper是一个Go应用程序配置系统。它旨在在一个应用程序内部工作,提供所有的配置需求。
Viper可以从JSON、TOML、YAML、HCL、envfile和Java属性配置文件等多种格式加载配置,同时支持从环境变量、远程配置系统、命令行标志解析配置。
Viper提供了一些强大的功能:
- 设置默认配置值
- 通过配置文件、环境变量等覆盖默认配置
- 将配置参数绑定到Go变量
- 热加载配置文件
- 加密/解密配置值
- 将ENV变量绑定到配置
- 从远程配置系统拉取配置
- 查看使用的配置值
- 验证配置值
安装
go get github.com/spf13/viper
开始使用
-
创建配置文件
config.yaml
-
在代码中读取配置:
viper.SetConfigFile("config.yaml")
viper.ReadInConfig()
port := viper.GetString("port")
- 访问配置值,例如
viper.GetString("port")
为什么使用Viper
构建一个现代应用程序时,你不希望担心配置文件格式,只想专注于构建出色的软件。Viper正是来帮助您的。
Viper为您做了以下工作:
- 找到、加载并解析JSON、TOML、YAML、HCL、INI、envfile或Java属性格式的配置文件。
- 为不同的配置选项提供设置默认值的机制。
- 提供通过命令行标志覆盖配置的机制。
- 提供别名系统,可以轻松重命名参数而不破坏现有代码。
- 可以轻松区分用户提供的命令行或配置文件与默认值是否相同。
Viper使用以下优先级顺序。每个项都优先于它下面的项:
- 对
Set
的显式调用 - 命令行选项
- 环境变量
- 配置文件
- Key/Value存储
- 默认值
重要: Viper配置键是大小写不敏感的。
将值放入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") // 配置文件名称(无扩展名)
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: %w", err))
}
你可以这样处理没有找到配置文件的具体情况:
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// 没有找到配置文件;如果需要可以忽略错误
} else {
// 找到配置文件,但产生了其它错误
}
}
// 找到并成功解析了配置文件
注意 [从1.6开始]: 你也可以没有扩展名的文件,并以编程方式指定格式。 对于那些没有任何扩展名的位于用户主目录的配置文件,如 .bashrc
写入配置文件
读取配置文件很有用,但有时你会要将运行时的所有修改写入存储。 为此,提供了一系列命令,每个命令都有特定的用途:
- WriteConfig - 将当前的viper配置写入预定义的路径,如果不存在则失败。如果存在,将覆盖当前的配置文件。
- SafeWriteConfig - 将当前的viper配置写入预定义的路径。如果不存在则失败。如果存在,不会覆盖当前的配置文件。
- WriteConfigAs - 将当前的viper配置写入给定的文件路径。如果存在将覆盖给定的文件。
- SafeWriteConfigAs - 将当前的viper配置写入给定的文件路径。如果存在不会覆盖给定的文件。
大致的规则是,标记为“safe”的都不会覆盖任何文件,仅在不存在时创建,而默认行为是创建或截断。
下面是一小段示例:
viper.WriteConfig() // 将当前配置写入通过 'viper.AddConfigPath()' 和 'viper.SetConfigName' 设置的预定义路径。
viper.SafeWriteConfig()
viper.WriteConfigAs("/path/to/my/.config")
viper.SafeWriteConfigAs("/path/to/my/.config") // 会报错,因为已经写入了
viper.SafeWriteConfigAs("/path/to/my/.other_config")
监视和重新读取配置文件
Viper支持让你的应用程序在运行时动态读取配置文件。
不再需要重新启动服务器就可以重载配置,使用viper的应用可以在运行时读取配置更新。
只需要告诉viper实例监视配置即可。 还可以可选地为Viper提供每次变化发生时要运行的函数。
在调用 WatchConfig()
之前,请确保已经添加了所有配置路径
viper.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("Config file changed:", e.Name)
})
viper.WatchConfig()
从io.Reader读取配置
Viper预定义了许多配置源,如文件、环境变量、标志和远程K/V存储,但你不必受限于它们。 你还可以实现自己所需的配置源并提供给viper。
viper.SetConfigType("yaml") // 或者 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"
设置优先覆盖
可以来自命令行标志或应用程序逻辑本身。
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完全支持环境变量。这使得开箱即用的12因子应用成为可能。有五种方法可以辅助使用ENV:
AutomaticEnv()
BindEnv(string...) : error
SetEnvPrefix(string)
SetEnvKeyReplacer(string...) *strings.Replacer
AllowEmptyEnv(bool)
使用ENV变量时,需要认识到Viper将ENV变量视为区分大小写。
Viper提供一种机制来确保ENV变量唯一。通过使用 SetEnvPrefix
,你可以告诉Viper在读取环境变量时使用前缀。BindEnv
和 AutomaticEnv
都将使用这个前缀。
BindEnv
接收一个或多个参数。第一个参数是键名,其余的是要绑定到该键的环境变量名称。如果提供多个,将按指定顺序优先。环境变量名称区分大小写。如果没有提供ENV变量名,则Viper将自动假定ENV变量匹配以下格式:前缀 + "_" + 键名全大写
。当你显式提供ENV变量名(第二个参数)时,它不会自动添加前缀。例如,如果第二个参数是“id”,Viper将查找ENV变量“ID”。
使用ENV变量时,需要认识到的值每次访问时都会读取。Viper不会在调用 BindEnv
时固定值。
AutomaticEnv
是一个强大的辅助程序,特别是与 SetEnvPrefix
组合使用时。调用时,Viper会在每次 viper.Get
请求时检查环境变量。它将应用以下规则:它将检查具有匹配上面描述的规则的环境变量。
SetEnvKeyReplacer
允许你使用 strings.Replacer
对象重写ENV键到一定程度。如果你想在 Get()
调用中使用 -
或类似内容,但希望环境变量使用 _
分隔符,这很有用。在 viper_test.go
中可以找到使用它的示例。
或者,你可以与 NewWithOptions
工厂函数一起使用 EnvKeyReplacer
。
与 SetEnvKeyReplacer
不同,它接受 StringReplacer
接口,允许你编写自定义的字符串替换逻辑。
默认情况下,空环境变量被视为未设置,并回退到下一个配置源。要将空环境变量视为设置,请使用 AllowEmptyEnv
方法。
Env 示例
SetEnvPrefix("spf") // 会自动转为大写
BindEnv("id")
os.Setenv("SPF_ID", "13") // 通常在应用程序外部完成
id := Get("id") // 13
监视etcd中的更改 - 未加密
// 另外,你可以创建一个新的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) : 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") // 键不区分大小写
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") // 返回 "127.0.0.1"
这遵循上面建立的优先规则;对键路径的搜索将级联运行其余配置注册表,直到找到为止。
例如,给定此配置文件,datastore.metric.host
和 datastore.metric.port
已经定义好了(并可能被覆盖)。 此外,如果 datastore.metric.protocol
在默认值中定义了,Viper也会找到它。
但是,如果 datastore.metric
被覆盖(通过标志、环境变量、Set()
方法等),则 datastore.metric
的所有子键都变为未定义,它们被更高优先级的配置级别“隐藏”了。
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") // 返回 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") // 返回 "0.0.0.0"
提取子树
开发可重用模块时,能够提取配置的子集并将其传递给模块非常有用。这样模块可以实例化多次,使用不同的配置。
例如,应用程序可能会为不同目的使用多个不同的缓存存储:
cache:
cache1:
max-items: 100
item-size: 64
cache2:
max-items: 200
item-size: 80
我们可以将缓存名称传递给模块(例如NewCache("cache1")
),但这需要对访问配置键进行奇怪的串联,并与全局配置更加隔离。
所以我们没有这样做,而是将表示配置子集的Viper实例传递给构造函数:
cache1Config := viper.Sub("cache.cache1")
if cache1Config == nil { // Sub返回nil时表示找不到键
panic("cache configuration not found")
}
cache1 := NewCache(cache1Config)
注意: 始终检查Sub
的返回值。如果找不到键,它会返回nil
。
在内部,NewCache
函数可以直接寻址max-items
和item-size
键:
func NewCache(v *Viper) *Cache {
return &Cache{
MaxItems: v.GetInt("max-items"),
ItemSize: v.GetInt("item-size"),
}
}
生成的代码很容易测试,因为它与主配置结构解耦,更易于重用(出于相同原因)。
解marshal
你还可以选择将所有或特定的值解marshal到结构、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 := viper.Unmarshal(&C)
if err != nil {
t.Fatalf("无法解码到结构体, %v", err)
}
如果你想对键本身包含点的配置(默认键分隔符)解marshal,则必须更改分隔符:
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还支持解marshal到嵌入式结构中:
/*
示例配置:
module:
enabled: true
token: 89h3f98hbwf987h3f98wenf89ehf
*/
type config struct {
Module struct {
Enabled bool
moduleConfig `mapstructure:",squash"`
}
}
// moduleConfig可以在模块特定的包中
type moduleConfig struct {
Token string
}
var C config
err := viper.Unmarshal(&C)
if err != nil {
t.Fatalf("无法解码到结构体, %v", err)
}
Viper使用github.com/mitchellh/m… 进行解marshal,默认使用mapstructure
标签。
解码自定义格式
Viper的一个常见需求是添加更多值格式和解码器。例如,将字符(点、逗号、分号等)分隔的字符串解析成切片。
这在Viper中已经可以使用mapstructure解码钩子实现了。
更多详情请阅读这篇博文。
Marshal到字符串
你可能需要将viper持有的所有设置marshal成字符串,而不是写入文件。
你可以对AllSettings()
返回的配置使用首选格式的marshaller。
import (
yaml "gopkg.in/yaml.v2"
// ...
)
func yamlStringSettings() string {
c := viper.AllSettings()
bs, err := yaml.Marshal(c)
if err != nil {
log.Fatalf("无法marshal配置到YAML: %v", err)
}
return string(bs)
}
Viper还是Vipers?
Viper准备好了就可以使用。 不需要配置或初始化就可以开始使用Viper。 由于大多数应用程序希望使用单个集中存储库进行配置,所以viper包提供了这一点。 它类似单例。
在上面的所有示例中,它们都演示了以单例方式使用viper。
使用多个viper
你也可以为应用程序创建许多不同的viper。 每个viper都具有自己唯一的一组配置和值。 每个viper都可以从不同的配置文件、键值存储等读取配置。 Viper包支持的所有功能都镜像为viper的方法。
示例:
x := viper.New()
y := viper.New()
x.SetDefault("ContentDir", "content")
y.SetDefault("ContentDir", "foobar")
//...
使用多个viper时,跟踪不同的viper是用户的责任。
常见问题解答
为什么称之为“Viper”?
答:Viper旨在成为Cobra的伙伴。 虽然两者都可以完全独立运行,但组合在一起它们可以形成一个强大的基础框架。
为什么称之为“Cobra”?
还有比指挥官更好的名字吗?
Viper是否支持大小写敏感键?
简短答案: 不支持。
Viper从各种源合并配置,其中许多源要么不区分大小写,要么与其他源使用不同的大小写(例如环境变量)。 为了在使用多个源时提供最佳体验,已经做出将所有键设为不区分大小写的决定。
我们尝试了几种实现大小写敏感的方法,但遗憾的是这并非易事。 我们可能会在Viper v2中试着实现它,但尽管一开始有些吵闹,它似乎不是很需要。
你可以通过填写此反馈表格来投票支持大小写敏感: forms.gle/R6faU74qPRP…
是否安全地并发读写viper?
不安全,你需要自己同步对viper的访问(例如使用sync
包)。 并发读写可能导致panic。
排障
请参阅 TROUBLESHOOTING.md。
开发
或者,在您的计算机上安装 Go,然后运行 make deps
来安装其余依赖项。
运行测试:
make test
运行linter:
make lint # 传递-j选项以并行运行
某些linter违规可以自动修复:
make fmt
许可证
该项目使用 MIT许可证。