今天来记录一下在 go-vue-admin 项目中学习到的第一个东西——Viper

在第一篇文章中,我们已经基本了解了这个项目中后端部分的基本框架以及主函数的内容。从主函数开始,第一条代码便是初始化 Viper。关于 Viper,我也已经不是第一次使用了,但之前确实是没有去了解过,所以要从头开始学习。

Viper 是什么

XXX是什么?这是我们去学习一个东西想到的第一个问题。那么 Viper是 什么,详细内容如下:

viper 是一个配置解决方案,拥有丰富的特性:

  • 支持 JSON/TOML/YAML/HCL/envfile/Java properties 等多种格式的配置文件;
  • 可以设置监听配置文件的修改,修改时自动加载新的配置;
  • 从环境变量、命令行选项和io.Reader中读取配置;
  • 从远程配置系统中读取和监听修改,如 etcd/Consul;
  • 代码逻辑中显示设置键值。

为什么选择 Viper ?

在构建现代应用程序时,你无需担心配置文件格式;你想要专注于构建出色的软件。Viper的出现就是为了在这方面帮助你的。

Viper能够为你执行下列操作:

  1. 查找、加载和反序列化JSONTOMLYAMLHCLINIenvfileJava properties格式的配置文件。
  2. 提供一种机制为你的不同配置选项设置默认值。
  3. 提供一种机制来通过命令行参数覆盖指定选项的值。
  4. 提供别名系统,以便在不破坏现有代码的情况下轻松重命名参数。
  5. 当用户提供了与默认值相同的命令行或配置文件时,可以很容易地分辨出它们之间的区别。

Viper会按照下面的优先级。每个项目的优先级都高于它下面的项目:

  • 显示调用Set设置值
  • 命令行参数(flag)
  • 环境变量
  • 配置文件
  • key/value存储
  • 默认值

重要: 目前Viper配置的键(Key)是大小写不敏感的。

如何使用

Viper 的使用非常简单,它需要很少的设置。设置文件名(SetConfigName)、配置类型(SetConfigType)、和搜索路径(AddConfigPath),然后调用ReadInConfig。viper 会自动根据类型来读取配置。使用时调用 viper.Get 方法获取键值。

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
package main

import (
"fmt"
"log"

"github.com/spf13/viper"
)

func main() {
viper.SetConfigName("config")
viper.SetConfigType("toml")
viper.AddConfigPath(".")
viper.SetDefault("redis.port", 6381)
err := viper.ReadInConfig()
if err != nil {
log.Fatal("read config failed: %v", err)
}

fmt.Println(viper.Get("app_name"))
fmt.Println(viper.Get("log_level"))

fmt.Println("mysql ip: ", viper.Get("mysql.ip"))
fmt.Println("mysql port: ", viper.Get("mysql.port"))
fmt.Println("mysql user: ", viper.Get("mysql.user"))
fmt.Println("mysql password: ", viper.Get("mysql.password"))
fmt.Println("mysql database: ", viper.Get("mysql.database"))

fmt.Println("redis ip: ", viper.Get("redis.ip"))
fmt.Println("redis port: ", viper.Get("redis.port"))
}

有几点需要注意:

  • 设置文件名时不要带后缀;
  • 搜索路径可以设置多个,viper 会根据设置顺序依次查找;
  • viper 获取值时使用section.key的形式,即传入嵌套的键名;
  • 默认值可以调用viper.SetDefault设置。

读取值

viper 提供了多种形式的读取方法。在上面的例子中,我们看到了Get方法的用法。Get方法返回一个interface{}的值,使用有所不便。

GetType系列方法可以返回指定类型的值。 其中,Type 可以为Bool/Float64/Int/String/Time/Duration/IntSlice/StringSlice。 但是请注意,如果指定的键不存在或类型不正确,GetType方法返回对应类型的零值

如果要判断某个键是否存在,使用IsSet方法。 另外,GetStringMapGetStringMapString直接以 map 返回某个键下面所有的键值对,前者返回map[string]interface{},后者返回map[string]stringAllSettingsmap[string]interface{}返回所有设置。

设置键值

viper 支持在多个地方设置,使用下面的顺序依次读取:

  • 调用Set显示设置的;
  • 命令行选项;
  • 环境变量;
  • 配置文件;
  • 默认值。

viper.Set

如果某个键通过viper.Set设置了值,那么这个值的优先级最高。

1
`viper.Set("redis.port", 5381) `

如果将上面这行代码放到程序中,运行程序,输出的redis.port将是 5381。

命令行选项

如果一个键没有通过viper.Set显示设置值,那么获取时将尝试从命令行选项中读取。 如果有,优先使用。viper 使用 pflag 库来解析选项。 我们首先在init方法中定义选项,并且调用viper.BindPFlags绑定选项到配置中:

1
2
3
4
5
6
func init() {
pflag.Int("redis.port", 8381, "Redis port to connect")

// 绑定命令行
viper.BindPFlags(pflag.CommandLine)
}

然后,在main方法开头处调用pflag.Parse解析选项。

环境变量

如果前面都没有获取到键值,将尝试从环境变量中读取。我们既可以一个个绑定,也可以自动全部绑定。

init方法中调用AutomaticEnv方法绑定全部环境变量:

1
2
3
4
5
6
7
8
9
10
func init() {
// 绑定环境变量
viper.AutomaticEnv()
}

func main() {
// 省略部分代码

fmt.Println("GOPATH: ", viper.Get("GOPATH"))
}

也可以单独绑定环境变量:

1
2
3
4
5
6
7
8
9
10
func init() {
// 绑定环境变量
viper.BindEnv("redis.port") //只传入一个参数,这个参数即表示键名,又表示环境变量名。
viper.BindEnv("go.path", "GOPATH") //传入两个参数,第一个参数表示键名,第二个参数表示环境变量名。
}

func main() {
// 省略部分代码
fmt.Println("go path: ", viper.Get("go.path"))
}

还可以通过viper.SetEnvPrefix方法设置环境变量前缀,这样一来,通过AutomaticEnv和一个参数的BindEnv绑定的环境变量, 在使用Get的时候,viper 会自动加上这个前缀再从环境变量中查找。

如果对应的环境变量不存在,viper 会自动将键名全部转为大写再查找一次。所以,使用键名gopath也能读取环境变量GOPATH的值。

配置文件

如果经过前面的途径都没能找到该键,viper 接下来会尝试从配置文件中查找。

默认值

使用 viper.SetDefault() 来设置默认值,以供使用。

读取配置

io.Reader中读取

viper 支持从io.Reader中读取配置。这种形式很灵活,来源可以是文件,也可以是程序中生成的字符串,甚至可以从网络连接中读取的字节流。

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
package main

import (
"bytes"
"fmt"
"log"

"github.com/spf13/viper"
)

func main() {
viper.SetConfigType("toml")
tomlConfig := []byte(`
app_name = "awesome web"

# possible values: DEBUG, INFO, WARNING, ERROR, FATAL
log_level = "DEBUG"

[mysql]
ip = "127.0.0.1"
port = 3306
user = "dj"
password = 123456
database = "awesome"

[redis]
ip = "127.0.0.1"
port = 7381
`)
err := viper.ReadConfig(bytes.NewBuffer(tomlConfig))
if err != nil {
log.Fatal("read config failed: %v", err)
}

fmt.Println("redis port: ", viper.GetInt("redis.port"))
}

Unmarshal

viper 支持将配置Unmarshal到一个结构体中,为结构体中的对应字段赋值。

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
package main

import (
"fmt"
"log"

"github.com/spf13/viper"
)

type Config struct {
AppName string
LogLevel string

MySQL MySQLConfig
Redis RedisConfig
}

type MySQLConfig struct {
IP string
Port int
User string
Password string
Database string
}

type RedisConfig struct {
IP string
Port int
}

func main() {
viper.SetConfigName("config")
viper.SetConfigType("toml")
viper.AddConfigPath(".")
err := viper.ReadInConfig()
if err != nil {
log.Fatal("read config failed: %v", err)
}

var c Config
viper.Unmarshal(&c)

fmt.Println(c.MySQL)
}

保存配置

有时候,我们想要将程序中生成的配置,或者所做的修改保存下来。viper 提供了接口!

  • WriteConfig:将当前的 viper 配置写到预定义路径,如果没有预定义路径,返回错误。将会覆盖当前配置;
  • SafeWriteConfig:与上面功能一样,但是如果配置文件存在,则不覆盖;
  • WriteConfigAs:保存配置到指定路径,如果文件存在,则覆盖;
  • SafeWriteConfig:与上面功能一样,但是入股配置文件存在,则不覆盖。

监听文件修改

viper 可以监听文件修改,热加载配置。因此不需要重启服务器,就能让配置生效。

只需要调用viper.WatchConfig,viper 会自动监听配置修改。如果有修改,重新加载的配置。

另外,还可以为配置修改增加一个回调:

1
2
3
viper.OnConfigChange(func(e fsnotify.Event) {
fmt.Printf("Config file:%s Op:%s\n", e.Name, e.Op)
})

这样文件修改时会执行这个回调。

总结

至此,在本项目中所用到的有关 Viper 的内容就记录完了。但是viper相关的设计还没结束,还有另外一个很重要的库 flag。那就再下一篇博客中记录吧,一些写太多容易猪脑过载。

参考