spf13/viper
GitHub: spf13/viper
Go 语言中最流行的完整配置解决方案,统一管理配置文件、环境变量、命令行参数和远程配置源。
Stars: 30122 | Forks: 2099

[](https://github.com/avelino/awesome-go#configuration)
[](https://repl.it/@sagikazarmark/Viper-example#main.go)
[](https://github.com/spf13/viper/actions?query=workflow%3ACI)
[](https://gitter.im/spf13/viper?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[](https://goreportcard.com/report/github.com/spf13/viper)

[](https://pkg.go.dev/mod/github.com/spf13/viper)
**带有獠牙的 Go 配置方案!**
许多 Go 项目都使用 Viper 构建,包括:
* [Hugo](http://gohugo.io)
* [EMC RexRay](http://rexray.readthedocs.org/en/stable/)
* [Imgur’s Incus](https://github.com/Imgur/incus)
* [Nanobox](https://github.com/nanobox-io/nanobox)/[Nanopack](https://github.com/nanopack)
* [Docker Notary](https://github.com/docker/Notary)
* [BloomApi](https://www.bloomapi.com/)
* [doctl](https://github.com/digitalocean/doctl)
* [Clairctl](https://github.com/jgsqware/clairctl)
* [Mercure](https://mercure.rocks)
* [Meshery](https://github.com/meshery/meshery)
* [Bearer](https://github.com/bearer/bearer)
* [Coder](https://github.com/coder/coder)
* [Vitess](https://vitess.io/)
## 安装
```
go get github.com/spf13/viper
```
## 为什么使用 Viper?
Viper 是一个完整的 Go 应用程序配置解决方案,包括 [12-Factor apps](https://12factor.net/#the_twelve_factors)。它被设计为可以在任何应用程序中工作,并能处理所有类型的配置需求和格式。它支持:
* 设置默认值
* 设置显式值
* 读取配置文件
* 动态发现跨多个位置的配置文件
* 从环境变量读取
* 从远程系统读取(例如 Etcd 或 Consul)
* 从命令行标志读取
* 从缓冲区读取
* 实时监控和更新配置
* 为配置键设置别名以便于重构
Viper 可以被视为满足您应用程序所有配置需求的注册表。
## 将值存入 Viper
Viper 可以从多个配置源读取数据,并将它们合并为一组配置键值对。
Viper 使用以下优先级顺序进行合并:
* 显式调用 `Set`
* flags
* 环境变量
* 配置文件
* 外部键/值存储
* 默认值
### 读取配置文件
Viper 加载配置文件只需最少的配置。Viper 目前支持:
* JSON
* TOML
* YAML
* INI
* envfile
* Java Propeties
单个 Viper 实例仅支持单个配置文件,但可以搜索多个路径以查找该文件。
以下是如何使用 Viper 搜索和读取配置文件的示例。应至少提供一个预期存在配置文件的路径。
```
// Name of the config file without an extension (Viper will intuit the type
// from an extension on the actual file)
viper.SetConfigName("config")
// Add search paths to find the file
viper.AddConfigPath("/etc/appname/")
viper.AddConfigPath("$HOME/.appname")
viper.AddConfigPath(".")
// Find and read the config file
err := viper.ReadInConfig()
// Handle errors
if err != nil {
panic(fmt.Errorf("fatal error config file: %w", err))
}
```
您可以处理找不到配置文件的特定情况。
```
var fileLookupError viper.FileLookupError
if err := viper.ReadInConfig(); err != nil {
if errors.As(err, &fileLookupError) {
// Indicates an explicitly set config file is not found (such as with
// using `viper.SetConfigFile`) or that no config file was found in
// any search path (such as when using `viper.AddConfigPath`)
} else {
// Config file was found but another error was produced
}
}
// Config file found and successfully parsed
```
### 写入配置文件
有时您可能希望存储运行时所做的所有配置修改。
```
// Writes current config to the path set by `AddConfigPath` and `SetConfigName`
viper.WriteConfig()
viper.SafeWriteConfig() // Like the above, but will error if the config file exists
// Writes current config to a specific place
viper.WriteConfigAs("/path/to/my/.config")
// Will error since it has already been written
viper.SafeWriteConfigAs("/path/to/my/.config")
viper.SafeWriteConfigAs("/path/to/my/.other_config")
```
根据经验,带有 `Safe` 前缀的方法不会覆盖任何现有文件,而其他方法则会。
### 监控和重新读取配置文件
需要重启服务器才能使配置生效的日子一去不复返了——由 Viper 驱动的应用程序可以在运行时读取配置文件的更新,而不会错过任何节拍。
也可以提供一个函数,供 Viper 在每次发生更改时运行。
```
// All config paths must be defined prior to calling `WatchConfig()`
viper.AddConfigPath("$HOME/.appname")
viper.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("Config file changed:", e.Name)
})
viper.WatchConfig()
```
### 从 `io.Reader` 读取配置
Viper 预定义了许多配置源,但您也可以实现自己所需的配置源。
```
viper.SetConfigType("yaml")
var yamlExample = []byte(`
hacker: true
hobbies:
- skateboarding
- snowboarding
- go
name: steve
`)
viper.ReadConfig(bytes.NewBuffer(yamlExample))
viper.Get("name") // "steve"
```
### 设置默认值
一个好的配置系统将支持默认值,如果某个键没有通过其他方式设置,则会使用默认值。
示例:
```
viper.SetDefault("ContentDir", "content")
viper.SetDefault("LayoutDir", "layouts")
viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "categories"})
```
### 设置覆盖值
Viper 允许显式设置配置,例如来自您自己的应用程序逻辑。
```
viper.Set("verbose", true)
viper.Set("host.port", 5899) // Set an embedded key
```
### 注册和使用别名
别名允许通过多个键引用单个值
```
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 完全支持环境变量。
```
// Tells Viper to use this prefix when reading environment variables
viper.SetEnvPrefix("spf")
// Viper will look for "SPF_ID", automatically uppercasing the prefix and key
viper.BindEnv("id")
// Alternatively, we can search for any environment variable prefixed and load
// them in
viper.AutomaticEnv()
os.Setenv("SPF_ID", "13")
id := viper.Get("id") // 13
```
* 默认情况下,空环境变量被视为未设置,并将回退到下一个配置源,除非使用了 `AllowEmptyEnv`。
* Viper 不会“缓存”环境变量——每次访问时都会读取该值。
* `SetEnvKeyReplacer` 和 `EnvKeyReplacer` 允许您重写环境变量键,这对于将 SCREAMING_SNAKE_CASE 环境变量与来自其他来源的 kebab-cased 配置值合并非常有用。
### 使用 Flags
Viper 能够绑定到 flags。具体来说,Viper 支持 [Cobra](https://github.com/spf13/cobra) 库中使用的 [pflag](https://github.com/spf13/pflag/)。
与环境变量一样,值不是在调用绑定方法时设置的,而是在访问时设置的。
对于单个 flags,`BindPFlag` 方法提供了此功能。
```
serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))
```
您还可以绑定一组现有的 pflags。
```
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
```
标准库 [flag](https://golang.org/pkg/flag/) 包不被直接支持,但可以通过 pflag 进行解析。
```
package main
import (
"flag"
"github.com/spf13/pflag"
)
func main() {
// Using standard library "flag" package
flag.Int("flagname", 1234, "help message for flagname")
// Pass standard library flags to pflag
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
pflag.Parse()
// Viper takes over
viper.BindPFlags(pflag.CommandLine)
}
```
可以通过实现 `FlagValue` 和 `FlagValueSet` 接口来完全避免使用 pflag。
```
// Implementing 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" }
viper.BindFlagValue("my-flag-name", myFlag{})
// Implementing FlagValueSet
type myFlagSet struct {
flags []myFlag
}
func (f myFlagSet) VisitAll(fn func(FlagValue)) {
for _, flag := range flags {
fn(flag)
}
}
fSet := myFlagSet{
flags: []myFlag{myFlag{}, myFlag{}},
}
viper.BindFlagValues("my-flags", fSet)
```
### 远程 Key/Value 存储支持
要在 Viper 中启用远程支持,请对 `viper/remote` 包进行空白导入。
```
import _ "github.com/spf13/viper/remote"
```
Viper 支持以下远程键/值存储。下面提供了每种存储的示例。
* Etcd 和 Etcd3
* Consul
* Firestore
* NATS
Viper 将读取从键/值存储中的路径检索到的配置字符串。
Viper 支持用 `;` 分隔的多个主机。例如:`http://127.0.0.1:4001;http://127.0.0.1:4002`。
#### 加密
Viper 使用 [crypt](https://github.com/sagikazarmark/crypt) 从键/值存储中检索配置,这意味着您可以加密存储配置值,如果您拥有正确的 GPG 密钥环,它们将自动解密。加密是可选的。
Crypt 有一个命令行助手,您可以使用它将配置放入您的键/值存储中。
```
$ go get github.com/sagikazarmark/crypt/bin/crypt
$ crypt set -plaintext /config/hugo.json /Users/hugo/settings/config.json
$ crypt get -plaintext /config/hugo.json
```
有关如何设置加密值或如何使用 Consul 的示例,请参阅 Crypt 文档。
### 远程 Key/Value 存储示例(未加密)
#### 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()
```
#### etcd3
```
viper.AddRemoteProvider("etcd3", "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
给定一个 Consul 键 `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
```
#### Firestore
```
viper.AddRemoteProvider("firestore", "google-cloud-project-id", "collection/document")
viper.SetConfigType("json") // Config's format: "json", "toml", "yaml", "yml"
err := viper.ReadRemoteConfig()
```
当然,您也可以使用 `SecureRemoteProvider`。
#### NATS
```
viper.AddRemoteProvider("nats", "nats://127.0.0.1:4222", "myapp.config")
viper.SetConfigType("json")
err := viper.ReadRemoteConfig()
```
### 远程 Key/Value 存储示例(加密)
```
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()
```
### 监控 Key/Value 存储更改
```
// 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
runtime_viper.Unmarshal(&runtime_conf)
}
}()
```
## 从 Viper 获取值
从 Viper 检索配置值的最简单方法是使用 `Get*` 函数。`Get` 将返回一个 any 类型,但可以使用 `Get` 函数检索特定类型。
请注意,如果找不到键,每个 `Get*` 函数将返回零值。要检查键是否存在,请使用 `IsSet` 方法。
嵌套键使用 `.` 作为分隔符,数字用于数组索引。给定以下配置:
```
{
"datastore": {
"metric": {
"host": "127.0.0.1",
"ports": [
5799,
6029
]
}
}
}
```
```
GetString("datastore.metric.host") // "127.0.0.1"
GetInt("host.ports.1") // 6029
```
如果存在与分隔键路径匹配的键,则返回其值。
```
{
"datastore.metric.host": "0.0.0.0",
"datastore": {
"metric": {
"host": "127.0.0.1"
}
}
}
```
```
GetString("datastore.metric.host") // "0.0.0.0"
```
### 配置子集
提取配置的子集通常很有用(例如,在开发应接受特定配置部分的可重用模块时)。
```
cache:
cache1:
item-size: 64
max-items: 100
cache2:
item-size: 80
max-items: 200
```
```
func NewCache(v *Viper) *Cache {
return &Cache{
ItemSize: v.GetInt("item-size"),
MaxItems: v.GetInt("max-items"),
}
}
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)
```
### 反序列化
您还可以选择使用 `Unmarshal*` 方法将配置反序列化为结构体、map 等。
```
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]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 还支持反序列化为嵌入结构体。
```
/*
Example config:
module:
enabled: true
token: 89h3f98hbwf987h3f98wenf89ehf
*/
type config struct {
Module struct {
Enabled bool
moduleConfig `mapstructure:",squash"`
}
}
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](https://github.com/go-viper/mapstructure) 来反序列化值,默认情况下使用 `mapstructure` 标签。
### 编组为字符串
您可能需要将 Viper 中保存的所有设置编组为字符串。您可以将您喜欢的格式的编组器与 `AllSettings` 返回的配置一起使用。
```
import (
yaml "go.yaml.in/yaml/v3"
)
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 中通过 mapstructure 解码钩子实现了。
在[此博客文章](https://sagikazarmark.hu/blog/decoding-custom-formats-with-viper/)中阅读更多信息。
## 常见问题解答
### 为什么叫“Viper”?
Viper 被设计为 [Cobra](https://github.com/spf13/cobra) 的[伙伴](http://en.wikipedia.org/wiki/Viper_(G.I._Joe))。虽然两者都可以完全独立运行,但它们一起构成了一对强大的组合,可以处理您大部分的应用程序基础需求。
### 我发现了一个错误或想要一个功能,我应该提交 issue 还是 PR?
是的,但有两点需要注意。
1. Viper 项目目前优先考虑向后兼容性和稳定性,而不是功能。
2. 功能可能会推迟到 Viper 2 形成之后。
### 可以使用多个 Viper 实例吗?
**tl;dr:** 可以。
每个实例都有自己独特的配置,并且可以从不同的配置源读取。Viper 包支持的所有函数都作为方法镜像在 Viper 实例上。
```
x := viper.New()
y := viper.New()
x.SetDefault("ContentDir", "content")
y.SetDefault("ContentDir", "foobar")
```
### Viper 应该是全局单例还是传递使用?
最佳实践是初始化一个 Viper 实例并在必要时传递它。
Viper 开箱即带有一个全局实例(单例)。虽然它使设置配置变得容易,但通常不鼓励使用它,因为它会使测试变得更难并可能导致意外行为。
全局实例将来可能会被弃用。有关更多详细信息,请参阅 [#1855](https://github.com/spf13/viper/issues/1855)。
### Viper 支持区分大小写的键吗?
**tl;dr:** 不支持。
Viper 合并来自各种来源的配置,其中许多要么不区分大小写,要么使用与其他来源不同的大小写(例如,env vars)。为了在使用多个来源时提供最佳体验,所有键都不区分大小写。
曾有过几次尝试实现区分大小写,但不幸的是这并非易事。我们可能会尝试在 [Viper v2](https://github.com/spf13/viper/issues/772) 中实现它,但尽管最初有噪音,似乎并没有那么多人要求它。
您可以通过填写此反馈表为区分大小写投票:https://forms.gle/R6faU74qPRPAzchZ9。
### 并发读写 Viper 实例安全吗?
不安全,您需要自己同步对 Viper 的访问(例如通过使用 `sync` 包)。并发读写会导致 panic。
## 故障排除
请参阅 [TROUBLESHOOTING.md](TROUBLESHOOTING.md)。
## 开发
**为了获得最佳的开发者体验,建议安装 [Nix](https://nixos.org/download.html) 和 [direnv](https://direnv.net/docs/installation.html)。**
_或者,在您的计算机上安装 [Go](https://go.dev/dl/),然后运行 `make deps` 以安装其余的依赖项。_
运行测试套件:
```
make test
```
运行 linters:
```
make lint # pass -j option to run them in parallel
```
一些 linter 违规行为可以自动修复:
```
make fmt
```
## 许可证
该项目根据 [MIT License](LICENSE) 授权。
标签:12-Factor应用, EVTX分析, Go语言, JSON解析, spf13, TOML支持, Viper, YAML解析, 后端开发, 实时重载, 应用程序配置, 开发库, 恶意代码分析, 日志审计, 环境变量, 程序破解, 软件供应链组件, 配置文件, 配置解决方案