从 Viper 中读取配置值
前文中我们介绍了各种将配置读入 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
。
访问嵌套的键
有如下配置文件 config.yaml
:
1 2 3 4 5 |
username:jianghushinian password:123456 server: ip:127.0.0.1 port:8080 |
可以通过 .
分隔符来访问嵌套字段。
1 |
viper.Get("server.ip") |
示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package main import ( "fmt" "github.com/spf13/viper" ) funcmain() { 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")) } |
执行以上示例代码得到如下输出:
1 2 3 4 5 |
$ go run main.go username: jianghushinian server: map[ip:127.0.0.1 port:8080] server.ip: 10.0.0.1 server.port: 8080 |
有一种情况是,配置中本就存在着叫 server.ip
的键,那么它会遮蔽 server
对象下的 ip
配置项。
现在 config.yaml
配置如下:
1 2 3 4 5 6 |
username:jianghushinian password:123456 server: ip:127.0.0.1 port:8080 server.ip:10.0.0.1 |
示例程序如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package main import ( "fmt" "github.com/spf13/viper" ) funcmain() { 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")) } |
执行以上示例代码得到如下输出:
1 2 3 4 5 |
$ go run main.go username: jianghushinian server: map[ip:127.0.0.1 port:8080] server.ip: 10.0.0.1 server.port: 8080 |
server.ip
打印结果为 10.0.0.1
,而不再是 server
map 中所对应的值 127.0.0.1
。
提取子树
当使用 Viper 读取 config.yaml
配置文件后,viper
对象就包含了所有配置,并能通过 viper.Get("server.ip")
获取子配置。
我们可以将这份配置理解为一颗树形结构,viper
对象就包含了这个完整的树,可以使用如下方法获取 server
子树。
1 |
srvCfg := viper.Sub("server") |
使用示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package main import ( "fmt" "github.com/spf13/viper" ) funcmain() { viper.SetConfigFile("./config.yaml") viper.ReadInConfig() // 获取 server 子树 srvCfg := viper.Sub("server") // 读取配置值 fmt.Printf("ip: %v\n", srvCfg.Get("ip")) fmt.Printf("port: %v\n", srvCfg.Get("port")) } |
执行以上示例代码得到如下输出:
1 2 3 |
$ go run main.go ip: 127.0.0.1 port: 8080 |
反序列化
Viper 提供了 2 个方法进行反序列化操作,以此来实现将所有或特定的值解析到结构体、map 等。
Unmarshal(rawVal interface{}) : error
:反序列化所有配置项。UnmarshalKey(key string, rawVal interface{}) : error
:反序列化指定配置项。
使用示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
package main import ( "fmt" "github.com/spf13/viper" ) type Config struct { Username string Password string // Viper 支持嵌套结构体 Server struct { IP string Port int } } funcmain() { viper.SetConfigFile("./config.yaml") viper.ReadInConfig() var cfg *Config if err := viper.Unmarshal(&cfg); err != nil { panic(err) } 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) } |
执行以上示例代码得到如下输出:
1 2 3 |
$ go run main.go cfg: &{Username:jianghushinian Password:123456 Server:{IP:127.0.0.1 Port:8080}} password: 123456 |
如果配置项的 key
本身就包含 .
,则需要修改分隔符。
示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
package main import ( "fmt" "github.com/spf13/viper" ) type Config struct { Chart struct { Values map[string]interface{} } } funcmain() { // 默认的键分隔符为 `.`,这里将其修改为 `::` v := viper.NewWithOptions(viper.KeyDelimiter("::")) v.SetDefault("chart::values", map[string]interface{}{ "ingress": map[string]interface{}{ "annotations": map[string]interface{}{ "traefik.frontend.rule.type": "PathPrefix", "traefik.ingress.kubernetes.io/ssl-redirect": "true", }, }, }) var cfg *Config if err := v.Unmarshal(&cfg); err != nil { panic(err) } fmt.Printf("cfg: %+v\n", cfg) } |
执行以上示例代码得到如下输出:
1 2 |
$ go run main.go cfg: &{Chart:{Values:map[ingress:map[annotations:map[traefik.frontend.rule.type:PathPrefix traefik.ingress.kubernetes.io/ssl-redirect:true]]]}} |
注意⚠️:Viper 在后台使用 mapstructure 来解析值,其默认情况下使用
mapstructure
tags。当我们需要将 Viper 读取的配置反序列到结构体中时,如果出现结构体字段跟配置项不匹配,则可以设置mapstructure
tags 来解决。
序列化
一个好用的配置包不仅能够支持反序列化操作,还要支持序列化操作。Viper 支持将配置序列化成字符串,或直接序列化到文件中。
序列化成字符串
我们可以将全部配置序列化配置为 YAML 格式字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
package main import ( "fmt" "github.com/spf13/viper" yaml "gopkg.in/yaml.v2" ) // 序列化配置为 YAML 格式字符串 funcyamlStringSettings()string { c := viper.AllSettings() // 获取全部配置 bs, _ := yaml.Marshal(c) // 根据需求序列化成不同格式 returnstring(bs) } funcmain() { viper.SetConfigFile("./config.yaml") viper.ReadInConfig() fmt.Printf(yamlStringSettings()) } |
执行以上示例代码得到如下输出:
1 2 3 4 5 6 |
$ go run main.go password: 123456 server: ip: 127.0.0.1 port: 8080 username: jianghushinian |
写入配置文件
Viper 还支持直接将配置序列化到文件中,提供了如下几个方法:
WriteConfig
:将当前的viper
配置写入预定义路径。如果没有预定义路径,则会报错。如果预定义路径已经存在配置文件,将会被覆盖。SafeWriteConfig
:将当前的viper
配置写入预定义路径。如果没有预定义路径,则会报错。如果预定义路径已经存在配置文件,不会覆盖,会报错。WriteConfigAs
: 将当前的viper
配置写入给定的文件路径。如果给定的文件路径已经存在配置文件,将会被覆盖。SafeWriteConfigAs
:将当前的viper
配置写入给定的文件路径。如果给定的文件路径已经存在配置文件,不会覆盖,会报错。
使用示例:
1 2 3 4 5 |
viper.WriteConfig() // 将当前配置写入由 `viper.AddConfigPath()` 和 `viper.SetConfigName` 设置的预定义路径。 viper.SafeWriteConfig() viper.WriteConfigAs("/path/to/my/.config") viper.SafeWriteConfigAs("/path/to/my/.config") // 将会报错,因为它已经被写入了。 viper.SafeWriteConfigAs("/path/to/my/.other_config") |
多实例对象
由于大多数应用程序都希望使用单个配置实例对象来管理配置,因此 viper 包默认提供了这一功能,它类似于一个单例。当我们使用 Viper 时不需要配置或初始化,Viper 实现了开箱即用的效果。
在上面的所有示例中,演示了如何以单例方式使用 Viper。我们还可以创建多个不同的 Viper 实例以供应用程序中使用,每个实例都有自己单独的一组配置和值,并且它们可以从不同的配置文件、key/value 存储等位置读取配置信息。
Viper 包支持的所有功能都被镜像为 viper
对象上的方法,这种设计思路在 Go 语言中非常常见,如标准库中的 log 包。
多实例使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package main import ( "fmt" "github.com/spf13/viper" ) funcmain() { 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")) } |
在这里,我创建了两个 Viper 实例 x
和 y
,它们分别从配置文件读取配置和通过默认值的方式设置配置,使用时互不影响,使用者可以自行管理它们的生命周期。
执行以上示例代码得到如下输出:
1 2 3 |
$ go run main.go x.username: jianghushinian y.username: 江湖十年 |
使用建议
Viper 提供了众多方法可以管理配置,在实际项目开发中我们可以根据需要进行使用。如果是小型项目,推荐直接使用 viper
实例管理配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
package main import ( "fmt" "github.com/spf13/viper" ) funcmain() { viper.SetConfigFile("./config.yaml") if err := viper.ReadInConfig(); err != nil { panic(fmt.Errorf("read config file error: %s \n", err.Error())) } // 监控配置文件变化 viper.WatchConfig() // use config... fmt.Println(viper.Get("username")) } |
如果是中大型项目,一般都会有一个用来记录配置的结构体,可以使用 Viper 将配置反序列化到结构体中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
package main import ( "fmt" "github.com/fsnotify/fsnotify" "github.com/spf13/viper" ) type Config struct { Username string Password string // Viper 支持嵌套结构体 Server struct { IP string Port int } } funcmain() { viper.SetConfigFile("./config.yaml") if err := viper.ReadInConfig(); err != nil { panic(fmt.Errorf("read config file error: %s \n", err.Error())) } // 将配置信息反序列化到结构体中 var cfg *Config if err := viper.Unmarshal(&cfg); err != nil { panic(fmt.Errorf("unmarshal config error: %s \n", err.Error())) } // 注册每次配置文件发生变更后都会调用的回调函数 viper.OnConfigChange(func(e fsnotify.Event) { // 每次配置文件发生变化,需要重新将其反序列化到结构体中 if err := viper.Unmarshal(&cfg); err != nil { panic(fmt.Errorf("unmarshal config error: %s \n", err.Error())) } }) // 监控配置文件变化 viper.WatchConfig() // use config... fmt.Println(cfg.Username) } |
需要注意的是,直接使用 viper
实例管理配置的情况下,当我们通过 viper.WatchConfig()
监听了配置文件变化,如果配置变化,则变化会立刻体现在 viper
实例对象上,下次通过 viper.Get()
获取的配置即为最新配置。但是在使用结构体管理配置时,viper
实例对象变化了,记录配置的结构体 Config
是不会自动更新的,所以需要使用 viper.OnConfigChange
在回调函数中重新将变更后的配置反序列化到 Config
中。
总结
本文探讨 Viper 的各种用法和使用场景,首先说明了为什么使用 Viper,它的优势是什么。
接着讲解了 Viper 包中最核心的两个功能:如何把配置值读入 Viper 和从 Viper 中读取配置值。Viper 对着两个功能都提供了非常多的方法来支持。
然后又介绍了如何用 Viper 来管理多份配置,即使用多实例。
对于 Viper 的使用我也给出了自己的建议,针对小型项目,推荐直接使用 viper
实例管理配置,如果是中大型项目,则推荐使用结构体来管理配置。
最后,Viper 正在向着 v2 版本迈进,欢迎读者在这里分享想法,也期待下次来写一篇 v2 版本的文章与读者一起学习进步。
联系我
- 微信:jianghushinian
- 邮箱:jianghushinian007@outlook.com
- 博客地址:https://jianghushinian.cn/
参考
- Viper 源码仓库:https://github.com/spf13/viper