返回

Go配置管理之第三方库viper

Viper 是一个功能齐全的 Go 应用程序配置库,用来管理配置。


Viper 是一个功能齐全的 Go 应用程序配置库,支持很多场景。它可以处理各种类型的配置需求和格式,包括设置默认值、从多种配置文件和环境变量中读取配置信息、实时监视配置文件等。无论是小型应用还是大型分布式系统,Viper 都可以提供灵活而可靠的配置管理解决方案。在本文中,我们将深入探讨 Viper 的各种用法和使用场景,以帮助读者更好地了解和使用 Viper 来管理应用程序配置。


为什么选择 Viper

当我们在做技术选型时,肯定要知道为什么选择某一项技术,而之所以选择使用 Viper 来管理应用程序的配置,Viper 官方给出了如下答案:

当构建应用程序时,你不想担心配置文件格式,只想专注于构建出色的软件。Viper 就是为了帮助我们解决这个问题而存在的。

Viper 可以完成以下工作:

  1. 查找、加载和反序列化 JSON、TOML、YAML、HCL、INI、envfile 或 Java Properties 等多种格式的配置文件。
  2. 为不同的配置选项设置默认值。
  3. 为通过命令行标志指定的选项设置覆盖值。
  4. 提供别名系统,以便轻松重命名配置项而不破坏现有代码。
  5. 可以轻松区分用户提供的命令行参数或配置文件中的值是否与默认值相同。
  6. 可以设置监听配置文件的修改,修改时自动加载新的配置;
  7. 从环境变量、命令行选项和io.Reader中读取配置;
  8. 从远程配置系统中读取和监听修改,如 etcd/Consul;
  9. 代码逻辑中显示设置键值。
  10. ……

注:关于上面第 5 点,我个人理解的使用场景是:

  1. 先从命令行参数或配置文件中读取配置。
  2. 可以使用 viper.IsSet(key) 方法判断用户是否设置了 key 所对应的 value,如果设置了,可以通过 viper.Get(key) 获取值。
  3. 调用 viper.SetDefault(key, default_value) 来设置默认值(默认值不会覆盖上一步所获取到的值)。 在第 2 步中可以拿到用户设置的值 value,在第 3 步中可以知道默认值 default_value,这样其实就可以判断两者是否相同了。

Viper 采用以下优先级顺序来加载配置,按照优先级由高到低排序如下:

  • 显式调用 viper.Set 设置的配置值
  • 命令行参数
  • 环境变量
  • 配置文件
  • key/value 存储
  • 默认值

注意 ⚠️:Viper 配置中的键不区分大小写,如 user/User/USER 被视为是相等的 key,关于是否将其设为可选,目前还在讨论中。

Viper 包中最核心的两个功能是:如何把配置值读入 Viper 和从 Viper 中读取配置值,接下来我将分别介绍这两个功能。


快速使用

安装:

1
go get -u github.com/spf13/viper

导入:

1
import "github.com/spf13/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
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.Fatalf("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"))
}

这里的配置文件格式是toml。toml 的语法很简单,快速入门请看TOML:简体中文

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# config.toml
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

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

编译、运行程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ go run main.go 
awesome web
DEBUG
mysql ip:  127.0.0.1
mysql port:  3306
mysql user:  dj
mysql password:  123456
mysql database:  awesome
redis ip:  127.0.0.1
redis port:  7381

有几点需要注意:

  • 设置文件名时不要带后缀;
  • 搜索路径可以设置多个,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{}返回所有设置。

 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
// 省略包名和 import 部分

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

  fmt.Println("protocols: ", viper.GetStringSlice("server.protocols"))
  fmt.Println("ports: ", viper.GetIntSlice("server.ports"))
  fmt.Println("timeout: ", viper.GetDuration("server.timeout"))

  fmt.Println("mysql ip: ", viper.GetString("mysql.ip"))
  fmt.Println("mysql port: ", viper.GetInt("mysql.port"))

  if viper.IsSet("redis.port") {
    fmt.Println("redis.port is set")
  } else {
    fmt.Println("redis.port is not set")
  }

  fmt.Println("mysql settings: ", viper.GetStringMap("mysql"))
  fmt.Println("redis settings: ", viper.GetStringMap("redis"))
  fmt.Println("all settings: ", viper.AllSettings())
}

我们在配置文件 config.toml 中添加protocolsports配置:

1
2
3
4
[server]
protocols = ["http", "https", "port"]
ports = [10000, 10001, 10002]
timeout = "3s"

编译、运行程序,输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ go run main.go 
protocols:  [http https port]
ports:  [10000 10001 10002]
timeout:  3s
mysql ip:  127.0.0.1
mysql port:  3306
redis.port is set
mysql settings:  map[database:awesome ip:127.0.0.1 password:123456 port:3306 user:dj]
redis settings:  map[ip:127.0.0.1 port:7381]
all settings:  map[app_name:awesome web log_level:DEBUG mysql:map[database:awesome ip:127.0.0.1 pa
ssword:123456 port:3306 user:dj] redis:map[ip:127.0.0.1 port:7381] server:map[ports:[10000 10001 1
0002] protocols:[http https port] timeout:3s]]

如果将配置中的redis.port注释掉,将输出redis.port is not set

上面的示例中还演示了如何使用time.Duration类型,只要是time.ParseDuration接受的格式都可以,例如3s2min1min30s等。

设置键值

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

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

viper.Set

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

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

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

viper.SetDefault

设置默认值,如果没有配置键,将使用默认值

1
viper.SetDefault("log_level", "INFO")

如果配置文件中没有配置log_level,那么将使用INFO


把配置值读入Viper

Viper 支持多种方式读入配置:

  • 设置默认配置值
  • 从配置文件读取配置
  • 监控并重新读取配置文件
  • io.Reader 读取配置
  • 从环境变量读取配置
  • 从命令行参数读取配置
  • 从远程 key/value 存储读取配置

我们一个一个来看。

设置默认配置值

一个好的配置系统应该支持默认值。Viper 支持使用 viper.SetDefault(key, value)key 设置默认值 value,在没有通过配置文件、环境变量、远程配置或命令行标志设置 key 所对应值的情况下,这很有用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import (
	"fmt"

	"github.com/spf13/viper"
)

func main() {
	// 设置默认配置
	viper.SetDefault("username", "arlettebrook")
	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"))
}

执行以上示例代码得到如下输出:

1
2
3
$ go run main.go 
username: arlettebrook
server: map[ip:127.0.0.1 port:8080]

从配置文件读取配置

Viper 支持从 JSON、TOML、YAML、HCL、INI、envfile 或 Java Properties 格式的配置文件中读取配置。Viper 可以搜索多个路径,但目前单个 Viper 实例只支持单个配置文件。Viper 不会默认配置任何搜索路径,将默认决定留给应用程序

主要有两种方式来加载配置文件:

  • 通过 viper.SetConfigFile() 指定配置文件,显式定义配置文件的路径、名称和扩展名。 Viper将使用它并且不检查任何配置路径。
  • 通过 viper.SetConfigName() 指定不带扩展名的配置文件,viper.SetConfigType()指定配置文件类型类型。然后通过 viper.AddConfigPath() 指定配置文件的搜索路径中,可以通过多次调用,来设置多个配置文件搜索路径。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
package main

import (
    "errors"
    "flag"
    "fmt"
    "log"

    "github.com/spf13/viper"
)

var (
    cfg = flag.String("c", "", "config file.")
)

func main() {
    flag.Parse()

    if *cfg != "" {
       viper.SetConfigFile(*cfg) // 指定配置文件(路径 + 配置文件名)
    } else {
       viper.AddConfigPath(".")             // 把当前目录加入到配置文件的搜索路径中
       viper.AddConfigPath("$HOME/.config") // 可以多次调用 AddConfigPath 来设置多个配置文件搜索路径
       viper.SetConfigName("config")
       viper.SetConfigType("toml") // 如果配置文件名中没有扩展名,则需要显式指定配置文件的格式// 指定配置文件名(没有扩展名)
    }

    // 读取配置文件
    if err := viper.ReadInConfig(); err != nil {
       var configFileNotFoundError viper.ConfigFileNotFoundError
       if errors.As(err, &configFileNotFoundError) {
          log.Fatalln(configFileNotFoundError.Error())
       }
       log.Fatalln(err)
    }

    fmt.Printf("using config file: %s\n", viper.ConfigFileUsed())

    // 读取配置值
    fmt.Printf("username: %s\n", viper.Get("username"))
}

viper.ConfigFileUsed()返回使用的配置文件的路径

假如有如下配置文件 config.yaml 与示例程序在同一目录中:

1
2
3
4
5
6
# config.yaml
username: arlettebrook
password: 123456
server:
  ip: 127.0.0.1
  port: 8080

执行以上示例代码得到如下输出:

1
2
3
$ go run main.go -c ./config.yaml
using config file: ./config.yaml
username: arlettebrook

监控并重新读取配置文件

Viper 支持在应用程序运行过程中实时读取配置文件,即热加载配置。

只需要调用 viper.WatchConfig() 即可开启此功能。

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

import (
	"fmt"
	"log"
	"time"

	"github.com/fsnotify/fsnotify"

	"github.com/spf13/viper"
)

func main() {
	viper.SetConfigFile("./config.yaml")

	// 注册每次配置文件发生变更后都会调用的回调函数
	viper.OnConfigChange(func(e fsnotify.Event) {
		fmt.Printf("config file changed: %s username:%s\n", e.Name, viper.Get("username"))
	})

	// 监控并重新读取配置文件,需要确保在调用前添加了所有的配置路径
	viper.WatchConfig()

	err := viper.ReadInConfig()
	if err != nil {
		log.Fatalln("加载配置文件错误:", err)
	}

	// 读取配置值
	fmt.Printf("未修改的username: %s\n", viper.Get("username"))

	// 阻塞程序,这个过程中可以手动去修改配置文件内容,观察程序输出变化,注意要保存。
	time.Sleep(time.Second * 10)

	// 读取配置值
	fmt.Printf("最终的username: %s\n", viper.Get("username"))
}

值得注意的是,在调用 viper.WatchConfig() 监控并重新读取配置文件之前,需要确保添加了所有的配置搜索路径。

并且,我们还可以通过 viper.OnConfigChange() 函数注册一个每次配置文件发生变更后都会调用的回调函数。

我们依然使用上面的 config.yaml 配置文件:

1
2
3
4
5
username: arlettebrook
password: 123456
server:
  ip: 127.0.0.1
  port: 8080

执行以上示例代码,并在程序阻塞的时候,手动修改配置文件中 username 后面分别追加1保存、2保存3保存,可以得到如下输出:

1
2
3
4
5
6
7
8
9
$ go run main.go
未修改的username: arlettebrook
config file changed: config.yaml username:arlettebrook1
config file changed: config.yaml username:arlettebrook1
config file changed: config.yaml username:arlettebrook12
config file changed: config.yaml username:arlettebrook12
config file changed: config.yaml username:arlettebrook123
config file changed: config.yaml username:arlettebrook123
最终的username: arlettebrook123

我这里修改一次回调函数不知道为什么执行了俩次,不过没有影响。

监听文件修改

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

import (
    "fmt"
    "log"
    "time"

    "github.com/spf13/viper"
)

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

    viper.WatchConfig()

    fmt.Println("redis port before sleep: ", viper.Get("redis.port"))
    time.Sleep(time.Second * 10)
    fmt.Println("redis port after sleep: ", viper.Get("redis.port"))
}

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

上面程序中,我们先打印redis.port的值,然后Sleep 10s。在这期间修改配置中redis.port的值,Sleep结束后再次打印。 发现打印出修改后的值:

1
2
redis port before sleep:  7381
redis port after sleep:  73810

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

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

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

viper 使用fsnotify这个库来实现监听文件修改的功能。

io.Reader 读取配置

Viper 支持从任何实现了 io.Reader 接口的配置源中读取配置。注意需要指定配置文件的类型,才能识别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
package main

import (
    "bytes"
    "fmt"
    "log"

    "github.com/spf13/viper"
)

func main() {
    viper.SetConfigType("yaml") // 或者使用 viper.SetConfigType("YAML")

    var yamlExample = []byte(`
username: arlettebrook
password: 123456
server:
  ip: 127.0.0.1
  port: 8080
`)

    err := viper.ReadConfig(bytes.NewBuffer(yamlExample))
    if err != nil {
       log.Fatalln("读取配置文件错误:", err)
    }

    // 读取配置值
    fmt.Printf("username: %s\n", viper.Get("username"))
}

这里我们通过 bytes.NewBuffer() 构造了一个 bytes.Buffer 对象,它实现了 io.Reader 接口,所以可以直接传递给 viper.ReadConfig() 来从中读取配置。

执行以上示例代码得到如下输出:

1
2
$ go run main.go
username: arlettebrook

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.Fatalf("read config failed: %v", err)
    }

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

从环境变量读取配置

Viper 还支持从环境变量读取配置,有 5 个方法可以帮助我们使用环境变量:

  • AutomaticEnv():使Viper检查环境变量是否与任何现有键(配置、默认值或标志)匹配。如果找到匹配的环境变量,它们将被加载到Viper中。

    • 开启自动匹配(根据前缀匹配)环境变量,加载到Viper中。如何没有前缀将加载所有环境变量。
  • BindEnv(string...) : error:绑定一个环境变量。需要一个或两个参数,第一个参数是配置项的键名(不区分大小写),第二个参数是环境变量的名称。如果未提供第二个参数,则 Viper 将假定环境变量名为:环境变量前缀_键名,且为全大写形式。例如环境变量前缀为 ENV,键名为 username,则环境变量名为 ENV_USERNAME。当显式提供第二个参数时,它不会自动添加前缀,也不会自动将其转换为大写。例如,使用 viper.BindEnv("username", "username") 绑定键名为 username 的环境变量,应该使用 viper.Get("username") 读取环境变量的值。

    在使用环境变量时,需要注意,每次访问它的值时都会去环境变量中读取。当调用 BindEnv 时,Viper 不会固定它的值。

  • SetEnvPrefix(string):可以告诉 Viper 在读取环境变量时使用的前缀。BindEnvAutomaticEnv 都将使用此前缀。例如,使用 viper.SetEnvPrefix("ENV") 设置了前缀为 ENV,并且使用 viper.BindEnv("username") 绑定了环境变量,在使用 viper.Get("username") 读取环境变量时,实际读取的 keyENV_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):当环境变量为空时(有键名而没有值的情况),默认会被认为是未设置的,并且程序将回退到下一个配置来源。要将空环境变量视为已设置,可以使用此方法。

  • viper.AllSettings()读取全部配置,只能获取到通过 BindEnv 绑定的环境变量,无法获取到通过 AutomaticEnv 绑定的环境变量

注意 ⚠️:

  1. Viper 在读取环境变量时,是不区分大小写的。如果指定的环境变量与绑定的大小写不一致,viper会自动大小写转换。
  2. 以下代码是在windows下的bash终端中运行的。

使用示例:

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

import (
    "fmt"
    "strings"

    "github.com/spf13/viper"
)

func main() {
    viper.SetEnvPrefix("env") // 设置读取环境变量前缀,会自动转为大写 ENV
    viper.AllowEmptyEnv(true) // 将空环境变量视为已设置

    viper.AutomaticEnv()          // 开启自动匹配(根据前缀匹配)环境变量,加载到Viper中
    _ = viper.BindEnv("username") // 也可以单独绑定某一个环境变量
    _ = viper.BindEnv("password")

    // 将键名中的 . 或 - 替换成 _
    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"))
    // fmt.Printf("GOPATH:%v\n",viper.Get("gopath")) // 请注释到前缀在取消注释运行

    // 读取全部配置,只能获取到通过 BindEnv 绑定的环境变量,无法获取到通过 AutomaticEnv 绑定的环境变量
    fmt.Println(viper.AllSettings())
}

执行以上示例代码得到如下输出:

1
2
3
4
5
$ ENV_USERNAME=arlettebrook ENV_SERVER_IP=127.0.0.1 ENV_PASSWORD= go run main.go
username: arlettebrook
password: 
server.ip: 127.0.0.1
map[password: username:arlettebrook]

环境变量

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

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

1
2
3
4
func init() {
  // 绑定环境变量
  viper.AutomaticEnv()
}

为了验证是否绑定成功,我们在main方法中将环境变量 GOPATH 打印出来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import (
    "fmt"

    "github.com/spf13/viper"
)

func init() {
    // 绑定环境变量
    viper.AutomaticEnv()
}

func main() {
    fmt.Println("GOPATH:", viper.Get("gopath"))
    fmt.Println("JAVA_HOME:", viper.Get("java_home"))
}

其他环境变量也是一样的,上面输出:

1
2
3
$ go run main.go
GOPATH: D:\GoSettings\GoPath
JAVA_HOME: E:\Java\jdk-17.0.5

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

 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"
)

func init() {
    // 绑定环境变量
    _ = viper.BindEnv("redis.port", "redis_port")
    _ = viper.BindEnv("username")
}

func main() {
    fmt.Println("redis port:", viper.Get("redis.port"))
    fmt.Println("username:", viper.Get("username"))
}

调用BindEnv方法,如果只传入一个参数,则这个参数既表示键名,又表示环境变量名。 如果传入两个参数,则第一个参数表示键名,第二个参数表示环境变量名。

上面将运行将输出:

1
2
3
$ username=arlettebrook REDIS_PORT=10809 go run main.go 
redis port: 10809
username: arlettebrook

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

另外,嵌套的配置键,绑定环境变量时必须指定环境变量名,因为 Viper 不会自动将点转换为下划线或其他分隔符。

但可以设置环境变量名的替换符,就可以不用知道第二个参数

1
2
// 将键名中的 . 或 - 替换成 _
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))

完整代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
    "fmt"
    "strings"

    "github.com/spf13/viper"
)

func init() {
    // 绑定环境变量
    _ = viper.BindEnv("redis.port")
    _ = viper.BindEnv("username-a")
    viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
}

func main() {
    fmt.Println("redis port:", viper.Get("redis.port"))
    fmt.Println("username-a:", viper.Get("username-a"))
}

演示输出:

1
2
3
$ username_A=arlettebrook REDIS_PORT=10809 go run main.go 
redis port: 10809
username-a: arlettebrook

从命令行参数读取配置

Viper 支持 pflag 包(它们其实都在 spf13 仓库下),能够绑定命令行标志,从而读取命令行参数。

BindEnv 类似,在调用绑定方法时,不会设置值,而是在每次访问时设置。这意味着我们可以随时绑定它,例如可以在 init() 函数中。

  • BindPFlag:对于单个标志,可以调用此方法进行绑定。
  • BindPFlags:可以绑定一组现有的标志集 pflag.FlagSet

示例程序如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
    "fmt"

    "github.com/spf13/pflag"
    "github.com/spf13/viper"
)

var (
    _ = pflag.StringP("username", "u", "", "help message for username")
    _ = pflag.StringP("password", "p", "", "help message for password")
)

func main() {
    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"))
}

执行以上示例代码得到如下输出:

1
2
3
$ go run main.go -u arlettebrook -p 123456
username: arlettebrook
password: 123456

因为 pflag 能够兼容标准库的 flag 包,所以我们也可以变相的让 Viper 支持 flag。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
    "flag"
    "fmt"

    "github.com/spf13/pflag"
    "github.com/spf13/viper"
)

func main() {
    _ = flag.String("username", "", "help message for username")

    pflag.CommandLine.AddGoFlagSet(flag.CommandLine) // 将 flag 命令行参数注册到 pflag
    pflag.Parse()

    _ = viper.BindPFlags(pflag.CommandLine)

    // 读取配置值
    fmt.Printf("username: %s\n", viper.Get("username"))
}

执行以上示例代码得到如下输出:

1
2
$ go run main.go --username arlettebrook
username: arlettebrook

如果你不使用 flag 或 pflag,则 Viper 还提供了 Go 接口的形式来支持其他 Flags,具体用法可以参考官方文档

命令行选项

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

 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/pflag"
    "github.com/spf13/viper"
)

func init() {
    pflag.Int("redis.port", 8381, "Redis port to connect")

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

func main() {
    pflag.Parse()

    viper.SetConfigFile("./config.toml")
    _ = viper.ReadInConfig()

    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"))

}

编译、运行程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ go run main.go --redis.port 9381
awesome web
DEBUG
mysql ip:  127.0.0.1
mysql port:  3306
mysql user:  dj
mysql password:  123456
mysql database:  awesome
redis ip:  127.0.0.1
redis port:  9381

如何不传入选项:

将使用环境变量的配置,没有,在使用配置文件的配置,没有,在使用默认,都没有,为对应类型的零值。

将使用配置文件的配置redis port: 7381,如果配置文件没有配置,才会使用默认值。这里没有默认值。

从远程 key/value 存储读取配置

要在 Viper 中启用远程支持,需要匿名导入 viper/remote 包:

1
import _ "github.com/spf13/viper/remote"

Viper 支持 etcd、Consul 等远程 key/value 存储,这里以 Consul 为例进行讲解。

首先需要准备 Consul 环境,最方便快捷的方式就是启动一个 Docker 容器:

1
2
3
4
5
6
$ docker run \
    -d \
    -p 8500:8500 \
    -p 8600:8600/udp \
    --name=badger \
    consul agent -server -ui -node=server-1 -bootstrap-expect=1 -client=0.0.0.0

Docker 容器启动好后,浏览器访问 http://localhost:8500/,即可进入 Consul 控制台,在 user/config 路径下编写 YAML 格式的配置。

Consul Config

使用 Viper 从 Consul 读取配置示例代码如下:

 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"
	_ "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: %s\n", viper.Get("server.ip"))
}

执行以上示例代码得到如下输出:

1
2
3
$ go run main.go
username: jianghushinian
server.ip: 127.0.0.1

笔记:如果你想停止通过 Docker 安装的 Consul 容器,则可以执行 docker stop badger 命令。如果需要删除,则可以执行 docker rm badger 命令。


从 Viper 中读取配置值

前文中我们介绍了各种将配置读入 Viper 的技巧,现在该学习如何使用这些配置了。

在 Viper 中,有如下几种方法可以获取配置值:

  • Get(key string) interface{}:获取配置项 key 所对应的值,key 不区分大小写,返回接口类型。
  • Get<Type>(key string) <Type>:获取指定类型的配置值, 可以是 Viper 支持的类型:GetBoolGetFloat64GetIntGetIntSliceGetStringGetStringMapGetStringMapStringGetStringSliceGetTimeGetDuration
  • AllSettings() map[string]interface{}:返回所有配置。根据我的经验,如果使用环境变量指定配置,则只能获取到通过 BindEnv 绑定的环境变量,无法获取到通过 AutomaticEnv 绑定的环境变量。
  • IsSet(key string) bool:值得注意的是,在使用 GetGet<Type> 获取配置值,如果找不到,则每个 Get 函数都会返回一个零值。为了检查给定的键是否存在,可以使用 IsSet 方法,存在返回 true,不存在返回 false

访问嵌套的键

有如下配置文件 config.yaml

1
2
3
4
5
username: arlettebrook
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
19
20
21
22
package main

import (
    "fmt"
    "log"

    "github.com/spf13/viper"
)

func main() {
    viper.SetConfigFile("./config.yaml")
    err := viper.ReadInConfig()
    if err != nil {
       log.Fatalln("加载配置文件失败:", err)
    }

    // 读取配置值
    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: arlettebrook
server: map[ip:127.0.0.1 port:8080]
server.ip: 127.0.0.1
server.port: 8080

有一种情况是,配置中本就存在着叫 server.ip 的键,那么它会遮蔽 server 对象下的 ip 配置项。

1
2
3
4
5
6
username: arlettebrook
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
19
20
21
22
package main

import (
    "fmt"
    "log"

    "github.com/spf13/viper"
)

func main() {
    viper.SetConfigFile("./config.yaml")
    err := viper.ReadInConfig()
    if err != nil {
       log.Println("加载配置文件出错:", err)
    }

    // 读取配置值
    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: arlettebrook
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
20
package main

import (
    "fmt"

    "github.com/spf13/viper"
)

func main() {
    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"))
    fmt.Printf("server.ip: %v\n", viper.Get("server.ip"))
}

执行以上示例代码得到如下输出:

1
2
3
4
$ go run main.go 
ip: 127.0.0.1
port: 8080
server.ip: 10.0.0.1

这里键没有出现覆盖的情况

反序列化

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

import (
	"fmt"
	"log"

	"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()

	var cfg Config
	if err := viper.Unmarshal(&cfg); err != nil {
		log.Fatalln("反序列化错误:", err)
	}

	var Password string
	if err := viper.UnmarshalKey("Password", &Password); err != nil {
		log.Fatalln("反序列化错误:", err)
	}

	fmt.Printf("cfg: %+v\n", cfg)
	fmt.Printf("Password: %s\n", Password)
}

执行以上示例代码得到如下输出:

1
2
3
$ go run main.go 
cfg: {Username:arlettebrook Password:123456 Server:{IP:10.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{}
    }
}

func main() {
    // 默认的键分隔符为 `.`,这里将其修改为 `::`
    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
3
$ 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 来解决。

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
45
46
47
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.Fatalf("read config failed: %v", err)
	}

	var c Config
	err = viper.Unmarshal(&c)
	if err != nil {
		log.Fatalf("反序列化失败:%v", err)
	}

	fmt.Println(c.MySQL)
}

编译,运行程序,输出:

1
2
$ go run main.go 
{127.0.0.1 3306 dj 123456 awesome}

序列化

一个好用的配置包不仅能够支持反序列化操作,还要支持序列化操作。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"
    "gopkg.in/yaml.v3"
)

// 序列化配置为 YAML 格式字符串
func yamlStringSettings() string {
    c := viper.AllSettings() // 获取全部配置
    bs, _ := yaml.Marshal(c) // 根据需求序列化成不同格式
    return string(bs)
}

func main() {
    viper.SetConfigFile("./config.yaml")
    _ = viper.ReadInConfig()

    fmt.Printf(yamlStringSettings())
}

执行以上示例代码得到如下输出:

1
2
3
4
5
6
$ go run main.go 
password: 123456
server:
    ip: 10.0.0.1
    port: 8080
username: arlettebrook

写入配置文件

Viper 还支持直接将配置序列化到文件中,提供了如下几个方法:

  • WriteConfig:将当前的 viper 配置写入预定义路径。如果没有预定义路径,则会报错。如果预定义路径已经存在配置文件,将会被覆盖。
  • SafeWriteConfig:将当前的 viper 配置写入预定义路径。如果没有预定义路径,则会报错。如果预定义路径已经存在配置文件,不会覆盖,会报错。
  • WriteConfigAs: 将当前的 viper 配置写入给定的文件路径。如果给定的文件路径已经存在配置文件,将会被覆盖。
  • SafeWriteConfigAs:将当前的 viper 配置写入给定的文件路径。如果给定的文件路径已经存在配置文件,不会覆盖,会报错。
  • 注意保存的文件类型要与配置类型一直,否则会报错config type could not be determined for XXX

使用示例:

1
2
3
4
5
6
viper.WriteConfig() // 将当前配置写入由 `viper.AddConfigPath()` 和 `viper.SetConfigFile()` 设置的预定义路径。类型就为配置类型。
viper.SafeWriteConfig() // 将会报错,因为它已经被写入了。

viper.WriteConfigAs("./cfg.yaml") // 文件类型要与配置类型一直,否则报错
viper.SafeWriteConfigAs("./cfg.yaml") // 将会报错,因为它已经被写入了。
viper.SafeWriteConfigAs("./cfg/cfg.yaml")
 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
package main

import (
    "fmt"

    "github.com/spf13/viper"
    "gopkg.in/yaml.v3"
)

// 序列化配置为 YAML 格式字符串
func yamlStringSettings() string {
    c := viper.AllSettings() // 获取全部配置
    bs, _ := yaml.Marshal(c) // 根据需求序列化成不同格式
    return string(bs)
}

func main() {
    viper.SetConfigFile("./config.yaml")
    _ = viper.ReadInConfig()

    fmt.Printf(yamlStringSettings())
    viper.Set("username", "哈哈哈")
    err := viper.WriteConfigAs("./cfg.yaml")
    if err != nil {
       panic(err)
    }
    fmt.Println("修改后的username:", viper.Get("username"))
}

输出:

1
2
3
4
5
6
7
$ go run main.go 
password: 123456
server:
    ip: 10.0.0.1
    port: 8080
username: arlettebrook
修改后的username: 哈哈哈
保存配置

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

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

下面我们通过程序生成一个config.toml配置:

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

import (
    "log"

    "github.com/spf13/viper"
)

func main() {
    viper.SetConfigName("config")
    viper.SetConfigType("toml")
    viper.AddConfigPath(".")

    viper.Set("app_name", "awesome web")
    viper.Set("log_level", "DEBUG")
    viper.Set("mysql.ip", "127.0.0.1")
    viper.Set("mysql.port", 3306)
    viper.Set("mysql.user", "root")
    viper.Set("mysql.password", "123456")
    viper.Set("mysql.database", "awesome")

    viper.Set("redis.ip", "127.0.0.1")
    viper.Set("redis.port", 6381)

    err := viper.SafeWriteConfig()
    if err != nil {
       log.Fatal("write config failed: ", err)
    }
}

多实例对象

由于大多数应用程序都希望使用单个配置实例对象来管理配置,因此 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
20
21
22
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"))

    viper.SetDefault("username", "默认单实例对象")
    fmt.Printf("viper.username: %v\n", viper.Get("username"))
}

在这里,我创建了两个 Viper 实例 xy,它们分别从配置文件读取配置和通过默认值的方式设置配置,使用时互不影响,使用者可以自行管理它们的生命周期。

执行以上示例代码得到如下输出:

1
2
3
4
$ go run main.go 
x.username: arlettebrook
y.username: 多实例对象
viper.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"
)

func main() {
    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
    }
}

func main() {
    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 版本的文章与读者一起学习进步。


参考

  1. Viper 源码仓库: https://github.com/spf13/viper
  2. 搬运:在 Go 中如何使用 Viper 来管理配置
  3. Go 每日一库之 viper