Go应用构建工具(2)--viper
Go应用构建工具(2)–viper
1. 概述
基本上所有的后端应用,都是需要用到配置项,可能小的项目配置项不多会选择命令行来传递,但是对于大项目来说,配置项可能会很多,全都用命令行传递那就麻烦死了,而且不好维护。
所以基本上都是会选择将配置项保存在配置文件中,在程序启动时加载和解析。
而Viper是Go生态中目前最受欢迎的配置相关的包,Viper能满足我们对配置的各种需求,能处理不同格式的配置文件。
viper支持:
- 设置默认值
- 从
JSON,TOMAL,YAML,HCL,envfile和java properties等配置文件中读取配置信息 - 实时监控和重新读取配置文件(可选)
- 从环境变量中读取
- 从远程配置系统(etcd或Consul)中获取配置信息,并监控变化
- 从命令行中读取配置
- 从buffer中读取配置
- 显示配置值
由上面可知,viper支持从不同的位置读取配置,但不同的位置是具有不同的优先级的,优先级高的配置会覆盖掉优先级低的相同配置,以下是优先级的排序(从高到低):
- 显式调用Set方法设置
- 命令行flag
- 环境变量
- 配置文件
- key/value存储
- 默认值
注意:目前的Viper配置的Key是不区分大小写的。(目前正在讨论是否将这一选项设置为可选)
2. 使用Viper
- 读取配置文件
Viper读取配置文件,至少需要知道去哪里查找配置文件,它支持搜索多个路径,但当前一个Viper实例值支持单个配置文件;
Viper默认不配置任何搜索路径,将决策权交给应用程序;
Viper支持JSON,TOML,YAML,HCL,INI,envfile和java properties等配置文件
示例如下:
首先在当前目录下创建一个config目录,在config目录下新建一个config.yaml文件:
mysql:database: personspassword: 123456port: 3306url: 127.0.0.1user: root
redis:port: 6379url: 127.0.0.1
root:loglevel: debugname: appport: 8080
package mainimport ("fmt""github.com/spf13/viper"
)func main() {viper.SetConfigName("config") // 指定配置文件名(没有扩展名)viper.SetConfigType("yaml") // 如果配置文件没有扩展名,则需要指定配置文件的格式,让viper知道如何解析文件viper.AddConfigPath("/etc/appname/") // 指定搜索路径viper.AddConfigPath("$HOME/.appname") // 可以调用多次,指定多个搜索路径viper.AddConfigPath("./config") // 当前工作目录下的config文件夹// 也可以使用SetConfigFile直接指定(上面的都不需要了)// viper.SetConfigFile("./config/config.yaml")err := viper.ReadInConfig() // 查找并读取配置文件if err != nil { // 处理错误panic(fmt.Errorf("fatal error config file: %w", err))}fmt.Printf("redis.Port = %v\n", viper.Get("redis.port"))
}
这里有几个点要注意:
- Viper支持配置多个搜索路径,但是需要注意添加的顺序,viper会根据添加的路径顺序搜索,如果找到了则停止搜索。
- 如果直接使用
SetConfigFile指定了配置文件路径和名字,必须显示带上文件扩展名,否则无法解析SetConfigName设置的配置文件名是不带扩展名的,在搜索时viper会加上扩展名- 使用
SetConfigName时,如果同一个目录下存在两个同名,但扩展名不一样的配置文件,比如config目录下存在:config.json和config.yaml,viper加载时只会是config.json文件;如果设置的扩展名是yaml,那么则是将config.json文件按照yaml解析
对于第4点的原因,跟踪了下源码,有一个切片,viper在加载时会按顺序匹配:var SupportedExts = []string{"json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl", "tfvars", "dotenv", "env", "ini"}
func (v *Viper) searchInPath(in string) (filename string) {v.logger.Debug("searching for config in path", "path", in)for _, ext := range SupportedExts {v.logger.Debug("checking if file exists", "file", filepath.Join(in, v.configName+"."+ext))if b, _ := exists(v.fs, filepath.Join(in, v.configName+"."+ext)); b {v.logger.Debug("found file", "file", filepath.Join(in, v.configName+"."+ext))return filepath.Join(in, v.configName+"."+ext)}}if v.configType != "" {if b, _ := exists(v.fs, filepath.Join(in, v.configName)); b {return filepath.Join(in, v.configName)}}return ""
}
对于配置文件没找到的错误处理也可以像这样子:
if err := viper.ReadInConfig(); err != nil {if _, ok := err.(viper.ConfigFileNotFoundError); ok {// Config file not found; ignore error if desired} else {// Config file was found but another error was produced}
}// Config file found and successfully parsed
Note:
从1.6版本起配置文件可以不带有扩展名,通过编程方式指定。譬如对于home目录下的配置文件是没有扩展名的,比如.bashrc
- 写入配置文件
通常我们使用配置文件大部分是用于读取配置,但是有时候希望保存运行时所做的修改,因此Viper提供了以下几个方法:
WriteConfig:将当前的viper配置写入预定义的路径(如果路径存在),这个方法会覆盖当前的配置文件(如果文件存在),预定义路径不存在则会报错SafeWriteConfig:与WriteConfig功能相似,不同之处是如果配置文件存在则不会覆盖WriteConfigAs:将当前的viper配置写入到指定的文件路径,如果文件存在则会覆盖SafeWriteConfigAs:与WriteConfigAs功能相似,不同之处是如果文件不存在不会覆盖
Note:带有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支持value默认值(key不需要默认值)。
示例如下:
viper.SetDefault("ContentDir", "content")
viper.SetDefault("LayoutDir", "layouts")
viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "categories"}
- 监听和重新读取配置文件
viper支持在运行时实时读取配置文件(就是热加载)。
只需要简单的告诉viper实例watchConfig即可监听,另外也可以提供一个回调方法给viper,用于每次有变动调用。
注意:在调用
WatchConfig()之前需要确认已经添加了配置文件的搜索路径
以下是示例:
viper.OnConfigChange(func(e fsnotify.Event) {fmt.Println("Config file changed:", e.Name)
})
viper.WatchConfig()
PS:回调方法里的参数fsnotify.Event只有改动的文件名和操作两个字段,感觉获取了也没啥用?
不太建议使用,比如修改了端口号,服务没有重启,服务还是监听在老的端口上,反而造成混淆
- 从io.Reader中读取配置
viper预先定义了一些配置源,比如配置文件,环境变量,flag和远程kv存储,但是我们还可以不使用这些,使用自己的配置源提供给viper。
具体看下示例(感觉用的也不多):
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: leathertrousers: denim
age: 35
eyes : brown
beard: true
`)viper.ReadConfig(bytes.NewBuffer(yamlExample))viper.Get("name") // this would be "steve"
-
显示设置配置
viper通过viper.Set()函数来进行显示设置配置,这种方式的优先级是最高的!
比如:viper.Set("redis.port",6666) -
注册和使用别名
别名允许一个值对用多个键
viper.RegisterAlias("loud", "Verbose") // 给Verbose注册一个别名loudviper.Set("verbose", true) // same result as next line
viper.Set("loud", true) // same result as prior lineviper.GetBool("loud") // true
viper.GetBool("verbose") // true
- 使用环境变量
viper支持环境变量,提供了以下5个方法来操作环境变量:
AutomaticEnv()BindEnv(string...): errorSetEnvPrefix(string)SetEnvKeyReplacer(string...) *strings.ReplacerAllowEmptyEnv(bool)
warning 注意
需要注意一点,使用环境变量时,viper是区分大小写的
viper提供了一种机制保证了环境变量是唯一的:SetEnvPrefix,通过SetEnvPrefix,可以告诉viper在读取环境变量时加上指定的前缀,BindEnv和AutomaticEnv也会使用这个前缀。
BindEnv有一个或多个参数,第一个参数是key名称,剩下的参数代表环境变量绑定到这个key,环境变量名称区分大小写;
如果环境变量的名称没有提供,那么viper会自动的认为环境变量匹配以下规则:prefix + _ + key name全大写,比如前缀为viper,BindEnv第一个参数为username,那么绑定的环境变量就是:VIPER_USERNAME;
如果提供了环境变量的名称(第二个参数),那么viper不会自动添加前缀,比如第二个参数是id,那么viper将会查找环境变量ID
一个重要的事情需要注意:每次访问环境变量的值时都会读取它,viper并不会在调用BindEnv时将值固定
AutomaticEnv是一个强大的助手,特别是配合SetEnvPrefix一起使用时。
当调用viper.Get时,viper会随时检查环境变量,它遵循这个规则:它会检查环境变量名称是否与key的大写名称匹配(如果key有设置了前缀则带上前缀)
SetEnvKeyReplacer允许我们使用strings.Replacer在一定程度上去重写环境变量的Key,比如当我们希望使用Get方法时用-分隔,但环境变量是以_分隔的,这种情况会比较有用。
比如,我们有环境变量USER_NAME=zhangsan,但是我们在调用viper.Get时希望是这样调用:viper.Get("user-name),那我们就可以这样子调用方法:
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) //会用_替换-
viper.Get("user-name")
一般来说空的环境变量会被认为是未设置的,并返回到下一个配置源,如果要将空的环境变量认为是已设置的,那么就可以使用AllowEmptyEnv方法
os.Setenv("VIPER_USER_NAME", "zhangsan")viper.AutomaticEnv()
viper.SetEnvPrefix("VIPER")
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
viper.BindEnv("user-name")fmt.Println("user-name = ", viper.Get("user-name"))
- 使用flag
viper可以绑定flag,确切的说,viper支持在cobra库中使用pflag。
与BindEnv类似,当调用绑定方法时不会设置值,而是在访问它时才会设置,这意味我们可以根据需要尽早绑定,即使是在init方法中。
对于单个flag,使用BindPflag()方法,比如:
var port = pflag.Int("port", 8888, "Input redis port")func main() {// 读取配置文件// viper.SetConfigFile("./config/config.yaml") // 指定配置文件路径viper.SetConfigName("config") // 配置文件名称(无扩展名)viper.SetConfigType("yaml") // 如果配置文件的名称中没有扩展名,需要此配置viper.AddConfigPath("/home") // 设置配置文件搜索路径viper.AddConfigPath("./config") // 设置多个搜索路径viper.SetDefault("redis.port", 65536)// viper.Set("redis.port", 6666)// 查找并读取配置文件err := viper.ReadInConfig()if err != nil {panic(fmt.Errorf("Fatal error config file: %w", err))}pflag.Parse()viper.BindPFlag("redis.port", pflag.CommandLine.Lookup("port"))redisPort := viper.Get("redis.port")fmt.Println("redis.port = ", redisPort)
}
也可以绑定一组现有的pflags(flag.FlagSet)
var port = pflag.Int("redis.port", 8888, "Input redis port")
var url = pflag.String("redis.url", "127.0.0.1", "Input redis url")func main() {// 读取配置文件viper.SetConfigName("config") // 配置文件名称(无扩展名)viper.SetConfigType("yaml") // 如果配置文件的名称中没有扩展名,需要此配置viper.AddConfigPath("/home") // 设置配置文件搜索路径viper.AddConfigPath("./config") // 设置多个搜索路径viper.SetDefault("redis.port", 65536)// viper.Set("redis.port", 6666)// 查找并读取配置文件err := viper.ReadInConfig()if err != nil {panic(fmt.Errorf("Fatal error config file: %w", err))}pflag.Parse()viper.BindPFlags(pflag.CommandLine)redisPort := viper.Get("redis.port")redisUrl := viper.GetString("redis.url")fmt.Println("redis.port = ", redisPort)fmt.Println("redis.port = ", redisUrl)
}
执行如下:
lucas@Z-NB-0406:~/workspace/test/pflagtest$ go run my_viper.go
redis.port = 63793
redis.port = 127.0.0.1
lucas@Z-NB-0406:~/workspace/test/pflagtest$ go run my_viper.go --redis.port 6666 --redis.url 10.2.3.222
redis.port = 6666
redis.port = 10.2.3.222
在viper中使用pflag并不会影响其他包使用标准库的flag包,pflag包可以通过导入来处理flag包定义的flag,这是通过pflag包提供的AddGoFlagSet()实现的(pflag那篇文章有介绍)
-
viper从远处kv存储获取(这部分目前我没etcd/Consul环境,暂时没去学习)
-
从Viper中获取值
viper提供了一些方法去获取值,这些方法根据值的类型区分,主要存在以下方法:
Get(key string) interface{}Get,比如(key string) GetString(),GetBool()AllSettings() map[string]interface{}IsSet(key string) bool
需要注意的一个点是:每一个Get方法当找不到key时都会返回对应类型的零值,要检查key是否存在,可以使用
IsSet()方法
- viper的读取key值的几种情况
- 访问嵌套的key
比如有以下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")
这遵循上面建立的规则,搜索路径将按优先级遍历其余配置,直到找到为止,比如在当前配置文件中没找到,就会继续向后查找,比如查找默认值
比如,在上面的配置文件中,同时定义了datastore.metric.host和datastore.metric.port(可以被覆盖),如果在默认值中定义了datastore.metric.protocol,viper也会找到它
然而,如果datastore.metric被直接覆盖了(flag,环境变量或者调用Set()等等),那么它的所有子key都会变成未定义,它们被高优先级别的配置遮盖了。
viper还可以通过使用number访问数组索引,比如:
{"host": {"address": "localhost","ports": [5799,6029]},"datastore": {"metric": {"host": "127.0.0.1","port": 3099},"warehouse": {"host": "198.0.0.1","port": 2112}}
}
使用: viper.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") // returns "0.0.0.0"
- 提取子树
在开发可重用模块时,提取配置的子集并将其传递给模块通常是有用的。
通过这种方式,模块可以使用不同的配置进行多次实例化。
比如,一个应用可能使用多个不同的cache配置
cache:cache1:max-items: 100item-size: 64cache2:max-items: 200item-size: 80
假如我们现在有一个NewCache方法:
func NewCache(v *Viper) *Cache {return &Cache {MaxItems: v.GetInt("max-items"),ItemSize: v.GetInt("item-size"),}
}
那么我们就可以通过提取子树获取对应配置传递给这个NewCache方法了:
cache1Config := viper.Sub("cache.cache1")
if cache1Config == nil {panic("cache configuration not found")
}cache1 := NewCache(cache1Config)
- 反序列化
viper支持将配置解析到struct或map等等,可以使用以下两个方法:Unmarshal(rawVal interface{}) errorUnmarshalKey(key string, rawVal interface{}) error
比如:
type config struct {Port intName stringPathMap string `mapstructure:"path_map"`
}var C configerr := 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]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 configv.Unmarshal(&C)
viper还支持解析到嵌入的结构中:
/*
Example config:module:enabled: truetoken: 89h3f98hbwf987h3f98wenf89ehf
*/
type config struct {Module struct {Enabled boolmoduleConfig `mapstructure:",squash"`}
}// moduleConfig could be in a module specific package
type moduleConfig struct {Token string
}var C configerr := viper.Unmarshal(&C)
if err != nil {t.Fatalf("unable to decode into struct, %v", err)
}
viper使用
github.com/mitchellh/mapstructure这个包来解析值,所以我们需要将viper反序列化到自定义的结构体变量中时,要使用mapstructure的tags
- 序列化为字符串
有时候我们可能需要将viper的所有配置序列化到一个字符串中,而不是将他们写入文件。我们也可以选择自己喜欢的格式序列化AllSettings()返回的所有配置。
示例如下:
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)
}
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
