文章首发于公众号:程序员读书;欢送关注,能够第一工夫收到文章更新哦,转载本文请注明起源!

前言

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

  • 命令行参数
  • 环境变量
  • 配置文件

显然,对于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"))//输入:nilfmt.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 mainimport (    "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有十分具体的理解,文章如果有写的不对的中央或者有什么须要补充的中央,欢送留言探讨!