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 的回调函数中,将变更好的最新值更新到“结构体配置对象”中。



