viper
Viper是什么?
Viper 是 Go 应用程序的完整配置解决方案,包括 12-Factor 应用程序。它旨在在应用程序中工作,并可以处理所有类型的配置需求和格式。它支持:
- 默认配置
- 从 JSON, TOML, YAML, HCL 和 Java 属性配置文件读取数据
- 实时查看和重新读取配置文件(可选)
- 从环境变量中读取
- 从远程配置系统(etcd 或 Consul)读取数据并监听变化
- 从命令行参数读取
- 从 buffer 中读取
- 设置显式值
Viper 可以被认为是所有应用程序配置需求的注册表。
Viper 使用以下优先级顺序。每个项目优先于其下方的项目:
- explicit call to Set
- flag
- env
- config
- key/value store
- default
- Viper配置键不区分大小写。
建立默认值
代码语言:go复制viper.SetDefault("ContentDir", "content")
viper.SetDefault("LayoutDir", "layouts")
viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "categories"})
读取配置文件
Viper 需要极少的配置让它知道去哪里查找配置文件。Viper 支持 JSON, TOML, YAML, HCL 和 Java 属性配置文件。Viper 可以搜索多个路径,但目前单个 Viper 实例仅 支持单个配置文件。Viper 不默认任何配置搜索路径,将默认决策留给应用程序。
代码语言:go复制viper.SetConfigName("config") // 配置文件的名字,不包括拓展名
viper.AddConfigPath("/etc/appname/") // 添加配置文件的路径
viper.AddConfigPath("$HOME/.appname") // 多次调用以添加多个搜索路径
viper.AddConfigPath(".") // .代表当前项目的工作目录
err := viper.ReadInConfig() //查找读取配置文件
if err != nil { // Handle errors reading the config file
panic(fmt.Errorf("Fatal error config file: %s n", err))
}
监听和重新读取配置文件
Viper 支持在运行时让应用程序实时读取配置文件。
只需告诉 viper 实例 watchConfig 即可。你可以选择为 Viper 提供当每次发生更改时运行的函数。
确保在调用 WatchConfig() 之前添加所有的配置路径 ConfigPath
代码语言: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")
覆盖设置
这些可以来自命令行标志,也可以来自你自己的应用程序逻辑。
代码语言:go复制viper.Set("Verbose", true)
viper.Set("LogFile", LogFile)
注册和使用别名
别名允许多个键引用单个值
代码语言:go复制viper.RegisterAlias("loud", "Verbose")
viper.Set("verbose", true)
viper.Set("loud", true)
viper.GetBool("loud") // true
viper.GetBool("verbose") // true
使用环境变量
Viper 完全支持环境变量。这使得 12 factor 应用程序可以开箱即用。 五种方法可以帮助使用 ENV:
代码语言:go复制AutomaticEnv()
BindEnv(string...) : error
SetEnvPrefix(string)
SetEnvKeyReplacer(string...) *strings.Replacer
AllowEmptyEnvVar(bool)
在处理环境变量时,重要的是要认识到 Viper 将环境变量视为区分大小写的变量。
Viper 提供了一种机制来确保 ENV 变量是唯一的。通过使用 SetEnvPrefix,可以告诉 Viper 在读取环境变量时使用前缀。BindEnv 和 AutomaticEnv 都将使用此前缀。
BindEnv 需要一个或两个参数。第一个参数是键名,第二个是环境变量的名称。环境变量的名称区分大小写。如果未提供 ENV 变量名,则 Viper 将自动假设键名与 ENV 变量名称匹配, 但 ENV 变量为 IN ALL CAPS。 当明确提供ENV变量名称时,它不会自动添加前缀。
使用 ENV 变量时要认识到的一件重要事情是每次访问时都会读取该值。当调用 BindEnv 时,Viper不会修复该值。
AutomaticEnv 是一个强大的帮手,特别是与 SetEnvPrefix 结合使用时。每当发出 viper.Get 请求时,Viper 都会检查环境变量。它将适用以下规则。 它将检查一个环境变量,其名称与大写的键匹配,如果设置了 EnvPrefix,则以它为前缀。
SetEnvKeyReplacer 允许你使用 strings.Replacer 对象来重写 Env 键。如果你想在 Get() 调用中使用 - 或者某些东西,但希望你的环境变量使用 _ 分隔符, 这是很有用的。使用它的一个例子可以在 viper_test.go 中找到。
默认情况下,空环境变量被视为未设置,并将回退到下一个配置源。要设置空环境变量,请使用 AllowEmptyEnv 方法。
代码语言:go复制SetEnvPrefix("spf") // 将自动大写
BindEnv("id")
os.Setenv("SPF_ID", "13")
id := Get("id") // 13
用Flags
Viper 具有绑定到标志的能力。具体来说,Viper支持Cobra库中使用的Pflag。
与BindEnv类似,该值不是在调用绑定方法时设置的,而是在访问该方法时设置的。这意味着你可以根据需要尽早进行绑定,即使在init()函数中也是如此。
对于单个标志,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") // 从viper而不是从pflag检索值
在 Viper 中使用 pflag 并不阻碍其他包中使用标准库中的 flag 包。pflag 包可以通过导入这些 flags 来处理flag包定义的flags。这是通过调用pflag包提供的便利函数AddGoFlagSet()来实现的。
例如:
代码语言: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") // 从 viper 检索值
...
}
从Viper获取值
在Viper中,有几种方法可以根据值的类型获取值。存在以下功能和方法:
代码语言:go复制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()方法。
例如:
代码语言:go复制viper.GetString("logfile") // 不区分大小写的设置和获取
if viper.GetBool("verbose") {
fmt.Println("verbose enabled")
}
访问嵌套的键
访问器方法也接受深度嵌套键的格式化路径。例如,如果加载下面的JSON文件:
代码语言:json复制{
"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")
如果存在与分隔的键路径匹配的键,则返回其值。例如:
代码语言:josn复制{
"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"
提取子树
从Viper中提取子树。
例如,viper实例现在代表了以下配置的yaml格式的文件:
代码语言:yaml复制app:
cache1:
max-items: 100
item-size: 64
cache2:
max-items: 200
item-size: 80
执行后:
subv := viper.Sub("app.cache1")
subv现在就代表:
代码语言:go复制max-items: 100
item-size: 64
假设我们现在有这么一个函数:
代码语言:go复制func NewCache(cfg *Viper) *Cache {...}
它基于subv格式的配置信息创建缓存。现在,可以轻松地分别创建这两个缓存,如下所示:
代码语言:go复制cfg1 := viper.Sub("app.cache1")
cache1 := NewCache(cfg1)
cfg2 := viper.Sub("app.cache2")
cache2 := NewCache(cfg2)
反序列化
你还可以选择将所有或特定的值解析到结构体、map等。
有两种方法可以做到这一点:
代码语言:go复制Unmarshal(rawVal interface{}) : error
UnmarshalKey(key string, rawVal interface{}) : 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]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还支持解析到嵌入的结构体:
代码语言: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中保存的所有设置序列化到一个字符串中,而不是将它们写入到一个文件中。你可以将自己喜欢的格式的序列化器与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实例。每个都有自己独特的一组配置和值。每个人都可以从不同的配置文件,key value存储区等读取数据。每个都可以从不同的配置文件、键值存储等中读取。viper包支持的所有功能都被镜像为viper实例的方法。
例如:
代码语言:go复制x := viper.New()
y := viper.New()
x.SetDefault("ContentDir", "content")
y.SetDefault("ContentDir", "foobar")
//...
当使用多个viper实例时,由用户来管理不同的viper实例。
使用Viper示例
假设我们的项目现在有一个./conf/config.yaml配置文件,内容如下:
代码语言:yaml复制port: 8123
version: "v1.2.3"
接下来通过示例代码演示两种在项目中使用viper管理项目配置信息的方式。
直接使用viper管理配置
这里用一个demo演示如何在gin框架搭建的web项目中使用viper,使用viper加载配置文件中的信息,并在代码中直接使用viper.GetXXX()方法获取对应的配置值。
代码语言:go复制package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
)
func main() {
viper.SetConfigFile("config.yaml") // 指定配置文件
viper.AddConfigPath("./conf/") // 指定查找配置文件的路径
err := viper.ReadInConfig() // 读取配置信息
if err != nil { // 读取配置信息失败
panic(fmt.Errorf("Fatal error config file: %s n", err))
}
// 监控配置文件变化
viper.WatchConfig()
r := gin.Default()
// 访问/version的返回值会随配置文件的变化而变化
r.GET("/version", func(c *gin.Context) {
c.String(http.StatusOK, viper.GetString("version"))
})
if err := r.Run(
fmt.Sprintf(":%d", viper.GetInt("port"))); err != nil {
panic(err)
}
}
使用结构体变量保存配置信息
我们还可以在项目中定义与配置文件对应的结构体,viper加载完配置信息后使用结构体变量保存配置信息。
代码语言:go复制package main
import (
"fmt"
"net/http"
"github.com/fsnotify/fsnotify"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
)
type Config struct {
Port int `mapstructure:"port"`
Version string `mapstructure:"version"`
}
var Conf = new(Config)
func main() {
viper.SetConfigFile("./conf/config.yaml") // 指定配置文件路径
err := viper.ReadInConfig() // 读取配置信息
if err != nil { // 读取配置信息失败
panic(fmt.Errorf("Fatal error config file: %s n", err))
}
// 将读取的配置信息保存至全局变量Conf
if err := viper.Unmarshal(Conf); err != nil {
panic(fmt.Errorf("unmarshal conf failed, err:%s n", err))
}
// 监控配置文件变化
viper.WatchConfig()
// 注意!!!配置文件发生变化后要同步到全局变量Conf
viper.OnConfigChange(func(in fsnotify.Event) {
fmt.Println("夭寿啦~配置文件被人修改啦...")
if err := viper.Unmarshal(Conf); err != nil {
panic(fmt.Errorf("unmarshal conf failed, err:%s n", err))
}
})
r := gin.Default()
// 访问/version的返回值会随配置文件的变化而变化
r.GET("/version", func(c *gin.Context) {
c.String(http.StatusOK, Conf.Version)
})
if err := r.Run(fmt.Sprintf(":%d", Conf.Port)); err != nil {
panic(err)
}
}