Vipper

vipper 是适用于 Go 应用程序的完整配置解析工具.他被设计用于在应用程序中工作.
鉴于 viper 库本身的 README 已经写得十分详细,这里就将其翻译成中文,并在最后附上两个项目中使用 viper 的示例代码以供参考。

安装

1
go get github.com/spf13/viper

介绍

Viper 是适用于 Go 应用程序(包括 Twelve-Factor App)的完整配置解决方案。它被设计用于在应用程序中工作,并且可以处理所有类型的配置需求和格式。它支持以下特性:

  • 设置默认值
  • JSONTOMLYAMLHCLenvfileJava properties格式的配置文件读取配置信息
  • 实时监控和重新读取配置文件(可选)
  • 从环境变量中读取
  • 从远程配置系统(etcd或Consul)读取并监控配置变化
  • 从命令行参数读取配置
  • 从buffer读取配置
  • 显式配置值

为什么选择viper

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

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

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

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

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

重要: 目前 Viper 配置的键(Key)是大小写不敏感的。目前正在讨论是否将这一选项设为可选。

建立默认值

一个好的配置系统应该支持默认值。键不需要默认值,但如果没有通过配置文件、环境变量、远程配置或命令行标志(flag)设置键,则默认值非常有用。

例如:

1
2
3
4
viper.SetDefault("ContentDir", "content")
viper.SetDefault("LayoutDir", "layouts")
viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "categories"})

读取配置文件

Viper 需要最少知道在哪里查找配置文件的配置。Viper 支持 JSONTOMLYAMLHCLenvfileJava properties 格式的配置文件。Viper 可以搜索多个路径,但目前单个 Viper 实例只支持单个配置文件。Viper 不默认任何配置搜索路径,将默认决策留给应用程序。

下面是一个如何使用 Viper 搜索和读取配置文件的示例。不需要任何特定的路径,但是至少应该提供一个配置文件预期出现的路径。

1
2
3
4
5
6
7
8
9
10
11
viper.SetConfigFile("./config.yaml") // 指定配置文件路径
viper.SetConfigName("config") // 配置文件名称(无扩展名)
viper.SetConfigType("yaml") // 如果配置文件的名称中没有扩展名,则需要配置此项
viper.AddConfigPath("/etc/appname/") // 查找配置文件所在的路径
viper.AddConfigPath("$HOME/.appname") // 多次调用以添加多个搜索路径
viper.AddConfigPath(".") // 还可以在工作目录中查找配置
err := viper.ReadInConfig() // 查找并读取配置文件
if err != nil { // 处理读取配置文件的错误
panic(fmt.Errorf("Fatal error config file: %s \n", err))
}

在加载配置文件出错时,你可以像下面这样处理找不到配置文件的特定情况:

1
2
3
4
5
6
7
8
9
10
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// 配置文件未找到错误;如果需要可以忽略
} else {
// 配置文件被找到,但产生了另外的错误
}
}

// 配置文件找到并成功解析

写入配置文件

从配置文件中读取配置文件是有用的,但是有时你想要存储在运行时所做的所有修改。为此,可以使用下面一组命令,每个命令都有自己的用途:

  • WriteConfig - 将当前的viper配置写入预定义的路径并覆盖(如果存在的话)。如果没有预定义的路径,则报错。
  • SafeWriteConfig - 将当前的viper配置写入预定义的路径。如果没有预定义的路径,则报错。如果存在,将不会覆盖当前的配置文件。
  • WriteConfigAs - 将当前的viper配置写入给定的文件路径。将覆盖给定的文件(如果它存在的话)。
  • SafeWriteConfigAs - 将当前的viper配置写入给定的文件路径。不会覆盖给定的文件(如果它存在的话)。

根据经验,标记为safe的所有方法都不会覆盖任何文件,而是直接创建(如果不存在),而默认行为是创建或截断。

一个小示例:

1
2
3
4
5
6
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实例watchConfig。可选地,你可以为Viper提供一个回调函数,以便在每次发生更改时运行。
确保在调用 WatchConfig() 之前添加了所有的配置路径。

1
2
3
4
5
6
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
// 配置文件发生变更之后会调用的回调函数
fmt.Println("Config file changed:", e.Name)
})

从 io.Reader 读配置

Viper 预先定义了许多配置源,如文件、环境变量、标志和远程 K/V 存储,但你不受其约束。你还可以实现自己所需的配置源并将其提供给 viper。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
viper.SetConfigType("yaml") // 或者 viper.SetConfigType("YAML")

// 任何需要将此配置添加到程序中的方法。
var yamlExample = []byte(`
Hacker: true
name: steve
hobbies:
- skateboarding
- snowboarding
- go
clothing:
jacket: leather
trousers: denim
age: 35
eyes : brown
beard: true
`)

viper.ReadConfig(bytes.NewBuffer(yamlExample))

viper.Get("name") // 这里会得到 "steve"

从 Viper 获取值

在 Viper 中,有几种方法可以根据值的类型获取值。存在以下功能和方法:

  • Get(key string) : interface{}
  • GetBool(key string) : bool
  • GetFloat64(key string) : float64
  • GetInt(key string) : int
  • GetIntSlice(key string) : []int
  • GetString(key string) : string
  • GetStringMap(key string) : map[string]interface{}
  • GetStringMapString(key string) : map[string]string
  • GetStringSlice(key string) : []string
  • GetTime(key string) : time.Time
  • GetDuration(key string) : time.Duration
  • IsSet(key string) : bool
  • AllSettings() : map[string]interface{}

需要认识到的一件重要事情是,每一个Get方法在找不到值的时候都会返回零值。为了检查给定的键是否存在,提供了IsSet()方法。

例如:

1
2
3
4
viper.GetString("logfile") // 不区分大小写的设置和获取
if viper.GetBool("verbose") {
fmt.Println("verbose enabled")
}

反序列化

你还可以选择将所有或特定的值解析到结构体、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
type config struct {
Port int
Name string
PathMap string `mapstructure:"path_map"`
}

var C config

err := viper.Unmarshal(&C)
if err != nil {
t.Fatalf("unable to decode into struct, %v", err)
}


如果你想要解析那些键本身就包含 . (默认的键分隔符)的配置,你需要修改分隔符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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",
},
},
})

type config struct {
Chart struct{
Values map[string]interface{}
}
}

var C config

v.Unmarshal(&C)

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
/*
Example config:

module:
enabled: true
token: 89h3f98hbwf987h3f98wenf89ehf
*/
type config struct {
Module struct {
Enabled bool

moduleConfig `mapstructure:",squash"`
}
}

// moduleConfig could be in a module specific package
type moduleConfig struct {
Token string
}

var C config

err := viper.Unmarshal(&C)
if err != nil {
t.Fatalf("unable to decode into struct, %v", err)
}

序列化成字符串

你可能需要将 viper 中保存的所有设置序列化到一个字符串中,而不是将它们写入到一个文件中。你可以将自己喜欢的格式的序列化器与 AllSettings() 返回的配置一起使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

import (
yaml "gopkg.in/yaml.v2"
// ...
)

func yamlStringSettings() string {
c := viper.AllSettings()
bs, err := yaml.Marshal(c)
if err != nil {
log.Fatalf("unable to marshal config to YAML: %v", err)
}
return string(bs)
}

项目实例记录

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
48
49
50
51
52
53
54
55
package core

import (
"flag"
"fmt"
"os"

"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
"main.go/global"
"main.go/utils"
)

func Viper(path ...string) *viper.Viper {
var config string
if len(path) == 0 {
flag.StringVar(&config, "c", "", "choose config file.")
flag.Parse()
if config == "" {
if configenv := os.Getenv(utils.ConfigEnv); configenv == "" {
config = utils.ConfigFile
fmt.Printf("您正在使用config的默认值,config的路径为%v\n", utils.ConfigFile)

} else {
config = configenv
fmt.Printf("您正在使用GVA+CONFIG环境变量,config的路径为%v\n", config)
}
} else {
fmt.Printf("您正在使用命令行的-c参数转递的值,config的路径为%v\n", config)
}
} else {
config = path[0]
fmt.Printf("您正在使用func Viper()传递的值,config的路径为%v\n", config)
}
v := viper.New()
v.SetConfigFile(config)
v.SetConfigType("yaml")
err := v.ReadInConfig()
if err != nil {
panic(fmt.Errorf("Fatal error config file:%s \n", err))

}
//监控后台线程配置文件
v.WatchConfig()
v.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("config file changed:", e.Name)
if err := v.Unmarshal(&global.GVA_CONFIG); err != nil {
fmt.Println(err)
}
})
if err := v.Unmarshal(&global.GVA_CONFIG); err != nil {
fmt.Println(err)
}
return v
}