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

  1. 读取配置文件
    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"))
}

这里有几个点要注意:

  1. Viper支持配置多个搜索路径,但是需要注意添加的顺序,viper会根据添加的路径顺序搜索,如果找到了则停止搜索。
  2. 如果直接使用SetConfigFile指定了配置文件路径和名字,必须显示带上文件扩展名,否则无法解析
  3. SetConfigName设置的配置文件名是不带扩展名的,在搜索时viper会加上扩展名
  4. 使用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

  1. 写入配置文件
    通常我们使用配置文件大部分是用于读取配置,但是有时候希望保存运行时所做的修改,因此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")
  1. 建立默认值
    Viper支持value默认值(key不需要默认值)。
    示例如下:
viper.SetDefault("ContentDir", "content")
viper.SetDefault("LayoutDir", "layouts")
viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "categories"}
  1. 监听和重新读取配置文件
    viper支持在运行时实时读取配置文件(就是热加载)。
    只需要简单的告诉viper实例watchConfig即可监听,另外也可以提供一个回调方法给viper,用于每次有变动调用。

注意:在调用WatchConfig()之前需要确认已经添加了配置文件的搜索路径
以下是示例:

viper.OnConfigChange(func(e fsnotify.Event) {fmt.Println("Config file changed:", e.Name)
})
viper.WatchConfig()

PS:回调方法里的参数fsnotify.Event只有改动的文件名和操作两个字段,感觉获取了也没啥用?
不太建议使用,比如修改了端口号,服务没有重启,服务还是监听在老的端口上,反而造成混淆

  1. 从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"
  1. 显示设置配置
    viper通过viper.Set()函数来进行显示设置配置,这种方式的优先级是最高的!
    比如:viper.Set("redis.port",6666)

  2. 注册和使用别名
    别名允许一个值对用多个键

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
  1. 使用环境变量
    viper支持环境变量,提供了以下5个方法来操作环境变量:
  • AutomaticEnv()
  • BindEnv(string...): error
  • SetEnvPrefix(string)
  • SetEnvKeyReplacer(string...) *strings.Replacer
  • AllowEmptyEnv(bool)

warning 注意
需要注意一点,使用环境变量时,viper是区分大小写的
viper提供了一种机制保证了环境变量是唯一的:SetEnvPrefix,通过SetEnvPrefix,可以告诉viper在读取环境变量时加上指定的前缀,BindEnvAutomaticEnv也会使用这个前缀。

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"))
  1. 使用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那篇文章有介绍)

  1. viper从远处kv存储获取(这部分目前我没etcd/Consul环境,暂时没去学习)

  2. 从Viper中获取值
    viper提供了一些方法去获取值,这些方法根据值的类型区分,主要存在以下方法:

  • Get(key string) interface{}
  • Get(key string) ,比如GetString(),GetBool()
  • AllSettings() map[string]interface{}
  • IsSet(key string) bool

需要注意的一个点是:每一个Get方法当找不到key时都会返回对应类型的零值,要检查key是否存在,可以使用IsSet()方法

  1. 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.hostdatastore.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{}) error
    • UnmarshalKey(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)
}


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部