写点什么

Golang 名库观止 | 配置解析神器 viper 使用详解

作者:程序员读书
  • 2022 年 5 月 12 日
  • 本文字数:5957 字

    阅读完需:约 20 分钟

Golang名库观止 | 配置解析神器viper使用详解

文章首发于公众号:程序员读书;欢迎关注,可以第一时间收到文章更新哦,转载本文请注明来源!

前言

对于现代应用程序,尤其大中型的项目来说,在程序启动和运行时,往往需要传入很多参数来控制程序的行为,这些参数可以通过以下几种方式传递给程序:


  • 命令行参数

  • 环境变量

  • 配置文件


显然,对于 Go 项目而言,单个去读取命令行、环境变量、配置文件并不难,但一个个读取却是很麻烦,有没有一个第三方库可以帮我们一次性读取上面几种数据源的配置呢?


有的,这里推荐使用viper库,viper 支持读取不同数据源和不同格式的配置文件,是 Go 项目读取配置的神器,今天跟着这篇文章,一起来探究一下吧!~

viper 简介

viper 是一个很完善的 Go 项目配置解决方案,很多著名的开源项目都在使用,比如Hugo,Docker都使用了该库,使用viper可以让我们专注于自己的项目代码,而不用自己写那些配置解析代码。

功能

  • 支持配置 key 默认值设置

  • 支持读取 JSON,TOML,YAML,HCL,envfile 和 java properties 等多种不同类型配置文件

  • 可以监听配置文件的变化,并重新加载配置文件

  • 读取系统环境变量的值

  • 读取存储在远程配置中心的配置数据,如 ectd,Consul,firestore 等系统,并监听配置的变化

  • 从命令行读取配置

  • 从 buffer 读取配置

  • 可以显示设置配置的值

viper 配置优先级

viper 支持从多个数据源读取配置值,因此当同一个配置 key 在多个数据源有值时,viper 读取的优先级如下:


  • 显示使用 Set 函数设置值

  • flag:命令行参数

  • env:环境变量

  • config:配置文件

  • key/value store:key/value 存储系统,如(etcd)

  • default:默认值


优先级示意图


安装 viper

viper 的安装非常简单,如同其他 Go 第三方包一样,只需要go get命令即可安装,如:


安装


go get github.com/spf13/viper
复制代码


使用


import "github.com/spf13/viper"
复制代码

支持哪些文件格式

我们一直在说,viper 支持多种不同格式的配置文件,到底是哪些格式呢?如下:


  • json

  • toml

  • yaml

  • yml

  • properties

  • props

  • prop

  • hcl

  • tfvars

  • dotenv

  • env

  • ini

key 大小写问题

viper 的配置的 key 值是不区分大小写,如:


# 小写的keyviper.set("test","this is a test value")# 大写的key,也可以读到值fmt.Println(viper.get("TEST"))//输出"this is a test value"
复制代码

使用指南

在了解了 viper 是什么之后,下面我们来看看要怎么使用 viper 去帮我们读取配置。

如何访问 viper 的功能

使用包名 viper,如:


viper.SetDefault("key1","value")//调用包级别下的函数
复制代码


使用viper.New()函数创建一个 Viper Struct,如:


viper := viper.New()viper.SetDefault("key2","value2")
复制代码


其实,这就是 Go 包的编程惯例,对实现功能对象再进行封装,并通过包名来调用。


因此,下面所有示例中调用函数使用 viper,可以是指包名 viper,或者通过 viper.New()返回的对象。

配置默认值

viper.SetDefault("key1","value1")viper.SetDefault("key2","value2")
复制代码

读取配置文件

直接指定文件路径


viper.SetConfigFile("./config.yaml")viper.ReadInConfig()fmt.Println(viper.Get("test"))
复制代码


多路径查找


viper.SetConfigName("config")     // 配置文件名,不需要后缀名viper.SetConfigType("yml")            // 配置文件格式viper.AddConfigPath("/etc/appname/")  // 查找配置文件的路径viper.AddConfigPath("$HOME/.appname") // 查找配置文件的路径viper.AddConfigPath(".")              // 查找配置文件的路径err := viper.ReadInConfig()           // 查找并读取配置文件if err != nil {                       // 处理错误    panic(fmt.Errorf("Fatal error config file: %w \n", err))}
复制代码


读取配置文件时,可能会出现错误,如果我们想判断是否是因为找不到文件而报错的,可以判断 err 是否为ConfigFileNotFoundError


if err := viper.ReadInConfig(); err != nil {  if _, ok := err.(viper.ConfigFileNotFoundError); ok {      } else {      }}
复制代码

写配置文件

除了读取配置文件外,viper 也支持将配置值写入配置文件,viper 提供了四个函数,用于将配置写回文件。

WriteConfig

WriteConfig 函数会将配置写入预先设置好路径的配置文件中,如果配置文件存在,则覆盖,如果没有,则创建。

SafeWriteConfig

SafeWriterConfig 与 WriteConfig 函数唯一的不同是如果配置文件存在,则会返回一个错误。

WriteConfigAs

WriteConfigAs 与 WriteConfig 函数的不同是需要传入配置文件保存路径,viper 会根据文件后缀判断写入格式。

SafeWriteConfigAs

SafeWriteConfigAs 与 WriteConfigAs 的唯一不同是如果配置文件存在,则返回一个错误。

监听配置文件

viper 支持监听配置文件,并会在配置文件发生变化,重新读取配置文件和回调函数,这样可以避免每次配置变化时,都需要重启启动应用的麻烦。


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

从 io.Reader 读取配置

除了支持从配置文件读取配置外,viper 也支持从实现了 io.Reader 接口的实例中读取配置(其实配置文件也实现了 io.Reader),如:


viper.SetConfigType("json") //设置格式
var yamlExample = []byte(`{ "name":"小明"}`)
viper.ReadConfig(bytes.NewBuffer(yamlExample))fmt.Println(viper.Get("name")) //输出“小明”
复制代码

显示设置配置项

也可以使用Set函数显示为某个 key 设置值,这种方式的优先级最高,会覆盖该 key 在其他地方的值,如:


viper.SetConfigType("json") //设置格式
var yamlExample = []byte(`{ "name":"小明"}`)
viper.ReadConfig(bytes.NewBuffer(yamlExample))fmt.Println(viper.Get("name")) //输出:小明
viper.Set("name","test")
fmt.Println(viper.Get("name"))//输出:test
复制代码

注册和使用别名

为某个配置 key 设置别名,这样可以方便我们在不改变 key 的情况下,使用别的名称访问该配置。


viper.Set("name", "test")
//为name设置一个username的别名viper.RegisterAlias("username", "name")
//通过username可以读取到name的值fmt.Println(viper.Get("username"))
//修改name的配置值,username的值也发生改变viper.Set("name", "小明")
fmt.Println(viper.Get("username"))
//修改username的值,name的值也发生改变viper.Set("username", "测试")
fmt.Println(viper.Get("name"))
复制代码

读取环境变量

对于读取操作系统环境变量,viper 提供了下面五个函数:


  • AutomaticEnv()

  • BindEnv(string...) : error

  • SetEnvPrefix(string)

  • SetEnvKeyReplacer(string...) *strings.Replacer

  • AllowEmptyEnv(bool)


要让 viper 读取环境变量,有两种方式:


  1. 调用 AutomaticEnv 函数,开启环境变量读取


fmt.Println(viper.Get("path"))
//开始读取环境变量,如果没有调用这个函数,则下面无法读取到path的值viper.AutomaticEnv()
//会从环境变量读取到该值,注意不用区分大小写fmt.Println(viper.Get("path"))
复制代码


  1. 使用 BindEnv 绑定某个环境变量


//将p绑定到环境变量PATH,注意这里第二个参数是环境变量,这里是区分大小写的viper.BindEnv("p", "PATH")
//错误绑定方式,path为小写,无法读取到PATH的值//viper.BindEnv("p","path")
fmt.Println(viper.Get("p"))//通过p可以读取PATH的值
复制代码


使用函数 SetEnvPrefix 可以为所有环境变量设置一个前缀,这个前缀会影响AutomaticEnvBindEnv函数


os.Setenv("TEST_PATH","test")
viper.SetEnvPrefix("test")
viper.AutomaticEnv()
//无法读取path的值,因为此时加上前缀,viper会去读取TEST_PATH这个环境变量的值fmt.Println(viper.Get("path"))//输出:nil
fmt.Println(viper.Get("test_path"))//输出:test
复制代码


环境变量大多是使用下划号(_)作为分隔符的,如果想替换,可以使用SetEnvKeyReplacer函数,如:


//设置一个环境变量os.Setenv("USER_NAME", "test")
//将下线号替换为-和.viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
//读取环境变量viper.AutomaticEnv()
fmt.Println(viper.Get("user.name"))//通过.访问fmt.Println(viper.Get("user-name"))//通过-访问fmt.Println(viper.Get("user_name"))//原来的下划线也可以访问
复制代码


默认的情况下,如果读取到的环境变量值为空(注意,不是环境变量不存在,而是其值为空),会继续向优化级更低数据源去查找配置,如果想阻止这一行为,让空的环境变量值有效,则可以调用AllowEmptyEnv函数:


viper.SetDefault("username", "admin")viper.SetDefault("password", "123456")
//默认是AllowEmptyEnv(false),这里设置为trueviper.AllowEmptyEnv(true)
viper.BindEnv("username")os.Setenv("USERNAME", "")
fmt.Println(viper.Get("username"))//输出为空,因为环境变量USERNAME空fmt.Println(viper.Get("password"))//输出:123456
复制代码

与命令行参数搭配使用

viper 可以和解析命令行库相关 flag 库一起工作,从命令行读取配置,其内置了对 pflag 库的支持,同时也留有接口让我们可以支持扩展其他的 flag 库

pflag

pflag.Int("port", 8080, "server http port")
pflag.Parse()viper.BindPFlags(pflag.CommandLine)
fmt.Println(viper.GetInt("port"))//输出8080
复制代码

扩展其他 flag

如果我们没有使用 pflag 库,但又想让 viper 帮我们读取命令行参数呢?


package main
import ( "flag" "fmt"
"github.com/spf13/viper")
type myFlag struct { f *flag.Flag}
func (m *myFlag) HasChanged() bool { return false}
func (m *myFlag) Name() string { return m.f.Name}func (m *myFlag) ValueString() string { return m.f.Value.String()}func (m *myFlag) ValueType() string { return "string"}
func NewMyFlag(f *flag.Flag) *myFlag { return &myFlag{f: f}}
func main() { flag.String("username", "defaultValue", "usage")
m := NewMyFlag(flag.CommandLine.Lookup("username"))
viper.BindFlagValue("myFlagValue", m)
flag.Parse()
fmt.Println(viper.Get("myFlagValue"))}
复制代码

远程 key/value 存储支持

viper 支持存储或者读取远程配置存储中心和 NoSQL(目前支持 etcd,Consul,firestore)的配置,并可以实时监听配置的变化,不过需要在代码中引入下面的包:


import _ "github.com/spf13/viper/remote"
复制代码


现在远程配置中心存储着以下 JSON 的配置信息


{  "hostname":"localhost",  "port":"8080"}
复制代码


那么我们可以通过下面的方面连接到系统,并读取配置,也可以单独开启一个 Goroutine 实时监听配置的变化。


连接 Consul


viper.AddRemoteProvider("consul", "localhost:8500", "MY_CONSUL_KEY")
复制代码


连接 etcd


viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001","/config/hugo.json")
复制代码


连接 firestore


viper.AddRemoteProvider("firestore", "google-cloud-project-id", "collection/document")
复制代码


连接上配置中心后,就可以像读取配置文件读取其中的配置了,如:


viper.SetConfigType("json")err := viper.ReadRemoteConfig()
fmt.Println(viper.Get("port")) // 输出:8080fmt.Println(viper.Get("hostname")) // 输出:localhost
复制代码


监听配置系统,如:


go func(){  for {    time.Sleep(time.Second * 5)     err := viper.WatchRemoteConfig()    if err != nil {      log.Errorf("unable to read remote config: %v", err)      continue    }  }}()
复制代码


另外,viper 连接 etcd,Consul,firestore 进行配置传输时,也支持加解密,这样可以更加安全,如果想要实现加密传输可以把AddRemoteProvider函数换为SecureRemoteProvider


viper.SecureRemoteProvider("etcd", "http://127.0.0.1:4001","/config/hugo.json","/etc/secrets/mykeyring.gpg")
复制代码

访问配置

viper 可以帮我们读取各个地方的配置,那读到配置之后,要怎么用呢?

直接访问

{  "mysql":{    "db":"test"  },  "host":{    "address":"localhost"    "ports":[      "8080",      "8081"    ]  }}
复制代码


对于多层级配置 key,可以用逗号隔号,如:


viper.Get("mysql.db")
viper.GetString("user.db")
viper.Get("host.address")//输出:localhost
复制代码


数组,可以用序列号访问,如:



viper.Get("host.posts.1")//输出: 8081
复制代码


也可以使用sub函数解析某个 key 的下级配置,如:


hostViper := viper.Sub("host")fmt.Println(hostViper.Get("address"))fmt.Println(hostViper.Get("posts.1"))
复制代码


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

序列化

读取了配置之后,除了使用上面列举出来的函数访问配置,还可以将配置序列化到 struct 或 map 之中,这样可以更加方便访问配置。


示例代码


配置文件:config.yaml


host: localhostusername: testpassword: testport: 3306charset: utf8dbName: test
复制代码


解析代码:


type MySQL struct {  Host     string  DbName   string  Port     string  Username string  Password string  Charset  string}
func main() {
viper.SetConfigName("config") viper.SetConfigType("yaml") viper.AddConfigPath(".") viper.ReadInConfig() var mysql MySQL
viper.Unmarshal(&mysql)//序列化
fmt.Println(mysql.Username) fmt.Println(mysql.Host)}
复制代码


对于多层级的配置,viper 也支持序列化到一个复杂的 struct 中,如:


我们将 config.yaml 改为如下结构:


mysql:   host: localhost  username: test  password: test  port: 3306  charset: utf8  dbName: testredis:   host: localhost  port: 6379
复制代码


示例程序



type MySQL struct { Host string DbName string Username string Password string Charset string}
type Redis struct { Host string Port string}
type Config struct { MySQL MySQL Redis Redis}
func main() {
viper.SetConfigName("config") viper.SetConfigType("yaml") viper.AddConfigPath(".") viper.ReadInConfig() var config Config
viper.Unmarshal(&config)
fmt.Println(config.MySQL.Username) fmt.Println(config.Redis.Host)}
复制代码

判断配置 key 是否存在

if viper.IsSet("user"){  fmt.Println("key user is not exists")}
复制代码

打印所有配置

m := viper.AllSettings()fmt.Println(m)
复制代码

小结

好了,文章写到了这里,已经很长了,相信如果看到这里的话,你应该对 viper 有非常详细的了解,文章如果有写的不对的地方或者有什么需要补充的地方,欢迎留言讨论!

发布于: 刚刚阅读数: 3
用户头像

后端开发 2018.04.05 加入

还未添加个人简介

评论

发布
暂无评论
Golang名库观止 | 配置解析神器viper使用详解_Go_程序员读书_InfoQ写作社区