Viper 是一个功能齐全的 Go 应用程序配置管理库,支持很多场景。它可以处理各种类型的配置需求和格式,包括设置默认值、从多种配置文件和环境变量中读取配置信息、实时监视配置文件等。无论是小型应用还是大型分布式系统,Viper 都可以提供灵活而可靠的配置管理解决方案。
一、基础知识与快速安装
1、为什么选择Viper?
在构建现代应用程序时,你无需担心配置文件格式;你想要专注于构建出色的软件。Viper的出现就是为了在这方面帮助你的。
Viper能够为你执行下列操作:
-
查找、加载和反序列化JSON、TOML、YAML、HCL、INI、envfile和Java properties格式的配置文件。
-
提供一种机制为你的不同配置选项设置默认值。
-
提供一种机制来通过命令行参数覆盖指定选项的值。
-
提供别名系统,以便在不破坏现有代码的情况下轻松重命名参数。
-
当用户提供了与默认值相同的命令行或配置文件时,可以很容易地分辨出它们之间的区别。
Viper 采用以下优先级顺序来加载配置,按照优先级由高到低排序如下:
-
显式调用 viper.Set 设置的配置值
-
命令行参数
-
环境变量
-
配置文件
-
key/value 存储
-
默认值
2、Viper的快速安装:
初始化go.mod:
go mod init myviper
安装viper
go get github.com/spf13/viper
如果想指定版本安装:
go get github.com/spf13/viper@v1.10.1
二、读取配置值写入Viper
1、设置默认配置值:
package main import ( "fmt" "github.com/spf13/viper" ) func main() { // 设置默认配置 viper.SetDefault("username", "jiguiquan") viper.SetDefault("server", map[string]string{"ip": "127.0.0.1", "port": "8080"}) // 读取配置值 fmt.Printf("username: %s\n", viper.Get("Username")) // key 不区分大小写 fmt.Printf("server: %+v\n", viper.Get("server")) }
运行结果如下:
username: jiguiquan server: map[ip:127.0.0.1 port:8080]
2、从配置文件读取配置:
Viper 支持从 JSON、TOML、YAML、HCL、INI、envfile 或 Java Properties 格式的配置文件中读取配置。Viper 可以搜索多个路径,但目前单个 Viper 实例只支持单个配置文件。
Viper 不会默认配置任何搜索路径,将默认决定留给应用程序。
主要有两种方式来加载配置文件:
-
通过 viper.SetConfigFile() 指定配置文件,如果配置文件名中没有扩展名,则需要使用 viper.SetConfigType() 显式指定配置文件的格式。
-
通过 viper.AddConfigPath() 指定配置文件的搜索路径中,可以通过多次调用,来设置多个配置文件搜索路径。然后通过 viper.SetConfigName() 指定不带扩展名的配置文件,Viper 会根据所添加的路径顺序查找配置文件,如果找到就停止查找。
先准备一个 config.yaml 配置文件:
username: jiguiquan password: 123456 server: ip: 127.0.0.1 port: 8080
在准备一个 config.properties 配置文件:
username=zidan password=123456 server.ip=127.0.0.1 server.port=8080
编写如下 go 程序:
package main import ( "errors" "flag" "fmt" "github.com/spf13/viper" ) var ( // 启动时,通过 -c 标识,传入配置文件路径 cfg = flag.String("c", "", "config file.") ) func main() { flag.Parse() if *cfg != "" { viper.SetConfigFile(*cfg) // 指定配置文件(路径 + 配置文件名) //viper.SetConfigType("yaml") // 如果配置文件名中没有扩展名,则需要显式指定配置文件的格式 } else { viper.AddConfigPath(".") // 把当前目录加入到配置文件的搜索路径中 viper.AddConfigPath("$HOME/.config") // 可以多次调用 AddConfigPath 来设置多个配置文件搜索路径 viper.SetConfigName("config") // 指定配置文件名(没有扩展名,会自己找,找到就停止) } // 读取配置文件 if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); ok { fmt.Println(errors.New("config file not found")) } else { fmt.Println(errors.New("config file was found but another error was produced")) } return } fmt.Printf("using config file: %s\n", viper.ConfigFileUsed()) // 读取配置值 fmt.Printf("username: %s\n", viper.Get("username")) }
验证1:指定配置文件读取:
E:\StudyGo\myviper>go run main.go -c ./config.properties using config file: ./config.properties username: zidan
验证2:不指定配置文件读取:
E:\StudyGo\myviper>go run main.go using config file: E:\StudyGo\myviper\config.yaml username: jiguiquan
3、监控并重新读取配置文件:
Viper 支持在应用程序运行过程中实时读取配置文件,即热加载配置。
只需要调用 viper.WatchConfig() 即可开启此功能。
编写 go 程序:
package main import ( "fmt" "time" "github.com/fsnotify/fsnotify" "github.com/spf13/viper" ) func main() { viper.SetConfigFile("./config.yaml") viper.ReadInConfig() // 注册每次配置文件发生变更后都会调用的回调函数 // 很多编辑器都会触发2次修改事件,注意回调逻辑的幂等性 viper.OnConfigChange(func(e fsnotify.Event) { fmt.Printf("配置文件发生改变: %s\n", e.Name) }) // 重要:监控并重新读取配置文件,需要确保在调用前添加了所有的配置路径 viper.WatchConfig() // 每隔10秒打印一次username配置最新值 for { // 读取配置值 fmt.Printf("username: %s\n", viper.Get("username")) time.Sleep(time.Second * 10) } }
运行结果:
E:\StudyGo\myviper>go run main.go username: jiguiquan 配置文件发生改变: config.yaml username: zidan username: zidan username: zidan
经过测试:当使用 Goland/Vscode/vim/vi 等编辑器编辑文件时,都会触发两次修改事件,当使用记事本修改时,只会触发一次事件;
官方解释是:编辑器的原因,编辑器发出了两次修改事件!
所以我们在开发时,要考虑到回调函数中的处理逻辑跌幂等性!
4、从 io.Reader 读取配置:
Viper 支持从任何实现了 io.Reader 接口的配置源中读取配置!
package main import ( "bytes" "fmt" "github.com/spf13/viper" ) func main() { viper.SetConfigType("yaml") // 或者使用 viper.SetConfigType("YAML") var yamlExample = []byte(` username: jiguiquan password: 123456 server: ip: 127.0.0.1 port: 8080 `) // bytes.NewBuffer() 构造了一个 bytes.Buffer 对象 // 而 bytes.NewBuffer() 是实现了 io.Reader 接口的 viper.ReadConfig(bytes.NewBuffer(yamlExample)) // 读取配置值 fmt.Printf("username: %s\n", viper.Get("username")) }
运行结果如下:
E:\StudyGo\myviper>go run main.go username: jiguiquan
5、从环境变量读取配置:
注意 ⚠:Viper 在读取环境变量时,是区分大小写的。
Viper 还支持从环境变量读取配置,有 5 个方法可以帮助我们使用环境变量:
-
AutomaticEnv():可以绑定全部环境变量(用法上类似 flag 包的 flag.Parse())。调用后,Viper 会自动检测和加载所有环境变量。
-
BindEnv(string…) : error:绑定一个环境变量。需要一个或两个参数,第一个参数是配置项的键名,第二个参数是环境变量的名称。如果未提供第二个参数,则 Viper 将假定环境变量名为:环境变量前缀_键名,且为全大写形式。例如环境变量前缀为 ENV,键名为 username,则环境变量名为 ENV_USERNAME。当显式提供第二个参数时,它不会自动添加前缀,也不会自动将其转换为大写。例如,使用 viper.BindEnv("username", "username") 绑定键名为 username 的环境变量,应该使用 viper.Get("username") 读取环境变量的值。
在使用环境变量时,需要注意,每次访问它的值时都会去环境变量中读取。当调用 BindEnv 时,Viper 不会缓存它的值。
-
SetEnvPrefix(string):可以告诉 Viper 在读取环境变量时使用的前缀。BindEnv 和 AutomaticEnv 都将使用此前缀。例如,使用 viper.SetEnvPrefix("ENV") 设置了前缀为 ENV,并且使用 viper.BindEnv("username") 绑定了环境变量,在使用 viper.Get("username") 读取环境变量时,实际读取的 key 是 ENV_USERNAME。
-
SetEnvKeyReplacer(string…) *strings.Replacer:允许使用 strings.Replacer 对象在一定程度上重写环境变量的键名。例如,存在 SERVER_IP="127.0.0.1" 环境变量,使用 viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) 将键名中的 . 或 – 替换成 _,则通过 viper.Get("server_ip")、viper.Get("server.ip")、viper.Get("server-ip") 三种方式都可以读取环境变量对应的值。
-
AllowEmptyEnv(bool):当环境变量为空时(有键名而没有值的情况),默认会被认为是未设置的,并且程序将回退到下一个配置来源。要将空环境变量视为已设置,可以使用此方法。
package main import ( "fmt" "strings" "github.com/spf13/viper" ) /** 需要先配置以下环境变量值 export username=jgq export ENV_USERNAME=jiguiquan export password=jgq123 export ENV_PASSWORD=jiguiquan123 export ENV_SERVER_IP=127.0.0.1 */ func main() { viper.SetEnvPrefix("env") // 设置读取环境变量前缀,会自动转为大写 ENV viper.AllowEmptyEnv(true) // 将空环境变量视为已设置 //viper.AutomaticEnv() // 可以绑定全部环境变量 viper.BindEnv("username") // 自动绑定到ENV_USERNAME环境变量 viper.BindEnv("password","password") // 绑定到password环境变量 viper.BindEnv("server.ip") // 自动绑定到ENV_SERVER_IP环境变量 // 将键名中的 . 或 - 替换成 _ viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) // 读取配置 fmt.Printf("username: %v\n", viper.Get("username")) fmt.Printf("password: %v\n", viper.Get("password")) fmt.Printf("server.ip: %v\n", viper.Get("server.ip")) // 读取全部配置,只能获取到通过 BindEnv 绑定的环境变量,无法获取到通过 AutomaticEnv 绑定的环境变量 fmt.Println(viper.AllSettings()) }
注意,在运行程序前,先设置代码前的那几个环境变量:
运行结果如下:
[root@tcosmo-szls01 myviper]# go run main.go username: jiguiquan # 读取的是ENV_USERNAME的值 password: jgq123 # 读取的是password的值 server.ip: 127.0.0.1 # 读取的是ENV_SERVER_IP的值 map[password:jgq123 server:map[ip:127.0.0.1] username:jiguiquan] # 只打印了通过 BindEnv 绑定的环境变量
6、从命令行参数读取配置:
Viper 支持 pflag 包(它们其实都在 spf13 仓库下),能够绑定命令行标志,从而读取命令行参数。
同 BindEnv 类似,在调用绑定方法时,不会设置值,而是在每次访问时设置。这意味着我们可以随时绑定它,例如可以在 init() 函数中。
BindPFlag:对于单个标志,可以调用此方法进行绑定。
BindPFlags:可以绑定一组现有的标志集 pflag.FlagSet。
package main import ( "fmt" "github.com/spf13/pflag" "github.com/spf13/viper" ) var ( // 自带的Flag是不支持 shorthand 标识简写的 username = pflag.StringP("username", "u", "", "help message for username") password = pflag.StringP("password", "p", "", "help message for password") ) func main() { // pflag 用法几乎和 Flag一样,兼容Flag pflag.Parse() viper.BindPFlag("username", pflag.Lookup("username")) // 绑定单个标志 viper.BindPFlags(pflag.CommandLine) // 绑定标志集 // 读取配置值 fmt.Printf("username: %s\n", viper.Get("username")) fmt.Printf("password: %s\n", viper.Get("password")) }
运行结果如下:
E:\StudyGo\myviper>go run main.go --username=jiguiquan -p 123456 username: jiguiquan password: 123456
7、从远程 key/value 存储读取配置:
通过阅读 viper.go 的源码,可以知道,暂时支持的远程provider有三种:etcd、consul、firestore
// provider is a string value: "etcd", "consul" or "firestore" are currently supported. func AddRemoteProvider(provider, endpoint, path string) error { return v.AddRemoteProvider(provider, endpoint, path) }
我们快速地在本地运行一个Consul:https://developer.hashicorp.com/consul/downloads
解压后直接以开发模式运行:
consul.exe agent -dev # 开发模式运行 consul.exe agent -server # 服务器模式运行
启动后,直接访问 http://localhost:8500 即可访问 Consul 界面:
我们在 user/config 创建一组yaml格式的配置:
配置准备好后,我们就可以开始编写 go 程序了:
package main import ( "fmt" "github.com/spf13/viper" _ "github.com/spf13/viper/remote" // 必须导入,才能加载远程 key/value 配置 ) func main() { viper.AddRemoteProvider("consul", "localhost:8500", "user/config") // 连接远程 consul 服务 viper.SetConfigType("YAML") // 显式设置文件格式文 YAML viper.ReadRemoteConfig() // 读取配置值 fmt.Printf("username: %s\n", viper.Get("username")) fmt.Printf("server.ip-port: %s-%d\n", viper.Get("server.ip"), viper.Get("server.port")) }
在运行时,可能需要安装对应的remote:
go get github.com/spf13/viper/remote # 或 go get github.com/spf13/viper/remote@v1.10.1
运行结果如下:
E:\StudyGo\myviper>go run main.go username: zidan-from-consul server.ip-port: 127.0.0.1-5600
正常能要用到读取场景也差不多就是上面这些!
三、从 Viper 中读取配置值
在 Viper 中,有如下几种方法可以获取配置值:
-
Get(key string) interface{}:获取配置项 key 所对应的值,key 不区分大小写,返回接口类型。
-
Get<Type>(key string) <Type>:获取指定类型的配置值, 可以是 Viper 支持的类型:GetBool、GetFloat64、GetInt、GetIntSlice、GetString、GetStringMap、GetStringMapString、GetStringSlice、GetTime、GetDuration。
-
AllSettings() map[string]interface{}:返回所有配置。根据我的经验,如果使用环境变量指定配置,则只能获取到通过 BindEnv 绑定的环境变量,无法获取到通过 AutomaticEnv 绑定的环境变量。
-
IsSet(key string) bool:值得注意的是,在使用 Get 或 Get<Type> 获取配置值,如果找不到,则每个 Get 函数都会返回一个零值。为了检查给定的键是否存在,可以使用 IsSet 方法,存在返回 true,不存在返回 false。
1、访问嵌套型key的配置项:
username: jiguiquan password: 123456 server: ip: 127.0.0.1 port: 8080 server.ip: 10.0.0.1 # 直接的 server.ip 优先级会高于嵌套型的配置值
编写的测试 go 代码如下:
package main import ( "fmt" "github.com/spf13/viper" ) func main() { viper.SetConfigFile("./config.yaml") viper.ReadInConfig() // 读取配置值 fmt.Printf("username: %v\n", viper.Get("username")) fmt.Printf("server: %v\n", viper.Get("server")) fmt.Printf("server.ip: %v\n", viper.Get("server.ip")) fmt.Printf("server.port: %v\n", viper.Get("server.port")) }
运行结果如下:
E:\StudyGo\myviper>go run main.go username: jiguiquan server: map[ip:127.0.0.1 port:8080] server.ip: 10.0.0.1 server.port: 8080
2、获取配置树,逐层获取配置值(树中的配置项不会被1中的场景覆盖):
在刚刚的代码中加上几行:
package main import ( "fmt" "github.com/spf13/viper" ) func main() { viper.SetConfigFile("./config.yaml") viper.ReadInConfig() // 读取配置值 fmt.Printf("username: %v\n", viper.Get("username")) fmt.Printf("server: %v\n", viper.Get("server")) fmt.Printf("server.ip: %v\n", viper.Get("server.ip")) fmt.Printf("server.port: %v\n", viper.Get("server.port")) // 获取 server 子树 srvCfg := viper.Sub("server") fmt.Printf("ip: %v\n", srvCfg.Get("ip")) fmt.Printf("port: %v\n", srvCfg.Get("port")) }
运行结果如下(很容易理解):
username: jiguiquan server: map[ip:127.0.0.1 port:8080] server.ip: 10.0.0.1 server.port: 8080 ip: 127.0.0.1 port: 8080
3、配置文件的反序列化:
Viper 提供了 2 个方法进行反序列化操作,以此来实现将所有或特定的值解析到结构体、map 等。
-
Unmarshal(rawVal interface{}) : error:反序列化所有配置项。
-
UnmarshalKey(key string, rawVal interface{}) : error:反序列化指定配置项。
package main import ( "fmt" "github.com/spf13/viper" ) type Config struct { Username string Password string // Viper 支持嵌套结构体 Server struct { IP string Port int } } func main() { viper.SetConfigFile("./config.yaml") viper.ReadInConfig() // 将配置文件反序列化为 cfg 对象 var cfg *Config if err := viper.Unmarshal(&cfg); err != nil { panic(err) } // 将单个key字段,反序列化为password字符串 var password *string if err := viper.UnmarshalKey("password", &password); err != nil { panic(err) } fmt.Printf("cfg: %+v\n", cfg) fmt.Printf("password: %s\n", *password) }
运行结果如下:
cfg: &{Username:jiguiquan Password:123456 Server:{IP:127.0.0.1 Port:8080}} password: 123456
4、序列化:
一个好用的配置包不仅能够支持反序列化操作,还要支持序列化操作。Viper 支持将配置序列化成字符串,或直接序列化到文件中;
package main import ( "fmt" "github.com/spf13/viper" ) func main() { viper.Set("username", "jiguiquan") viper.Set("password", "123") viper.Set("server", map[string]string{"ip": "192.168.0.1", "port": "8080"}) fmt.Println("Viper的所有配置如下:", viper.AllSettings()) viper.SafeWriteConfigAs("./myconfig.yaml") }
执行效果如下:
Viper的所有配置如下: map[password:123 server:map[ip:192.168.0.1 port:8080] username:jiguiquan]
同时还会生成一个配置文件:myconfig.yaml,内容如下:
password: "123" server: ip: 127.0.0.1 port: "8080" username: jiguiquan
四、多实例对象
由于大多数应用程序都希望使用单个配置实例对象来管理配置,因此 viper 包默认提供了这一功能,它类似于一个单例。当我们使用 Viper 时不需要配置或初始化,Viper 实现了开箱即用的效果。
在上面的所有示例中,演示了如何以单例方式使用 Viper。我们还可以创建多个不同的 Viper 实例以供应用程序中使用,每个实例都有自己单独的一组配置和值,并且它们可以从不同的配置文件、key/value 存储等位置读取配置信息。
Viper 包支持的所有功能都被镜像为 viper 对象上的方法,这种设计思路在 Go 语言中非常常见,如标准库中的 log 包。
多实例使用示例:
package main import ( "fmt" "github.com/spf13/viper" ) func main() { x := viper.New() y := viper.New() x.SetConfigFile("./config.yaml") x.ReadInConfig() fmt.Printf("x.username: %v\n", x.Get("username")) y.SetDefault("username", "吉桂权") fmt.Printf("y.username: %v\n", y.Get("username")) }
执行效果如下:
x.username: jiguiquan y.username: 吉桂权
五、项目实战建议
-
对于小型项目:推荐直接使用 viper 实例管理配置
-
对于大型项目:定义一个用来记录配置的结构体,使用 Viper 将配置反序列化到结构体中
但是当我们使用 viper.WatchConfig() 监听配置文件变化,如果配置变化,则变化会立刻体现在 viper 实例对象上,但是并不会改变我们的“结构体配置对象”,需要需要在 viper.OnConfigChange 的回调函数中,将变更好的最新值更新到“结构体配置对象”中。