文章首发于公众号:程序员读书;欢送关注,能够第一工夫收到文章更新哦,转载本文请注明起源!
前言
对于古代应用程序,尤其大中型的我的项目来说,在程序启动和运行时,往往须要传入很多参数来控制程序的行为,这些参数能够通过以下几种形式传递给程序:
- 命令行参数
- 环境变量
- 配置文件
显然,对于 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 值是不辨别大小写,如:
# 小写的 key
viper.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 读取环境变量,有两种形式:
- 调用 AutomaticEnv 函数,开启环境变量读取
fmt.Println(viper.Get("path"))
// 开始读取环境变量,如果没有调用这个函数,则上面无奈读取到 path 的值
viper.AutomaticEnv()
// 会从环境变量读取到该值,留神不必辨别大小写
fmt.Println(viper.Get("path"))
- 应用 BindEnv 绑定某个环境变量
// 将 p 绑定到环境变量 PATH, 留神这里第二个参数是环境变量,这里是辨别大小写的
viper.BindEnv("p", "PATH")
// 谬误绑定形式,path 为小写,无奈读取到 PATH 的值
//viper.BindEnv("p","path")
fmt.Println(viper.Get("p"))// 通过 p 能够读取 PATH 的值
应用函数 SetEnvPrefix 能够为所有环境变量设置一个前缀,这个前缀会影响 AutomaticEnv
和BindEnv
函数
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),这里设置为 true
viper.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")) // 输入:8080
fmt.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: localhost
username: test
password: test
port: 3306
charset: utf8
dbName: 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: test
redis:
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 有十分具体的理解,文章如果有写的不对的中央或者有什么须要补充的中央,欢送留言探讨!