Viper是适用于Go应用程序的完整配置解决方案。它被设计用于在应用程序中工作,并且可以处理所有类型的配置需求和格式。


一、安装

1
go get github.com/spf13/viper




二、介绍

2.1 什么是Viper?

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

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




2.2 为什么选择Viper?

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

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

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

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

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

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





三、存入viper

3.1 建立默认值

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

例如:

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




3.2 读取配置文件

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

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

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

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

注意[自1.6起]: 你也可以有不带扩展名的文件,并以编程方式指定其格式。对于位于用户$HOME目录中的配置文件没有任何扩展名,如.bashrc


这里补充两个问题供读者解答并自行验证

当你使用如下方式读取配置时,viper会从./conf目录下查找任何以config为文件名的配置文件,如果同时存在./conf/config.json./conf/config.yaml两个配置文件的话,viper会从哪个配置文件加载配置呢?

1
2
viper.SetConfigName("config")
viper.AddConfigPath("./conf")

在上面两个语句下搭配使用viper.SetConfigType("yaml")指定配置文件类型可以实现预期的效果吗?

AI回答

Viper 配置文件加载逻辑解析

一、默认情况下:同时存在 config.json 和 config.yaml 时的加载顺序

当使用以下代码时:

1
2
3
4
viper.SetConfigName("config")
viper.AddConfigPath("./conf")
// 未调用SetConfigType()
viper.ReadInConfig()

Viper 会按照内置的文件格式优先级顺序查找并加载第一个存在的文件。具体规则如下:

  1. Viper 支持的格式优先级顺序

Viper 内部维护了一个格式优先级列表,顺序为:

1
["json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv"]

因此,当./conf目录下同时存在config.jsonconfig.yaml时:

  • Viper 会先尝试加载config.json(因为json在列表中优先级高于yaml)。
  • config.json存在,则直接加载并忽略config.yaml;若不存在,才会继续查找config.yaml
  1. 验证示例

假设目录结构如下:

1
2
3
./conf/
├── config.json // 内容:{"port": 8080}
└── config.yaml // 内容:port: 9090

执行ReadInConfig()后,viper.Get("port")将返回8080(来自 json 文件),因为 json 优先级更高。


二、通过 SetConfigType (“yaml”) 指定类型能否达到预期?

可以,且会覆盖默认的格式优先级逻辑。具体行为如下:

  1. SetConfigType 的作用

调用viper.SetConfigType("yaml")后,Viper 会:

  • 忽略文件扩展名,直接按指定的格式(yaml)解析内容。
  • 强制查找名为config且格式为 yaml 的文件,即:
    • 优先查找config.yamlconfig.yml(因扩展名匹配),
    • 若不存在,也会尝试加载其他扩展名的文件(如config.txt),但要求文件内容符合 yaml 格式。
  1. 示例场景验证

当同时存在config.jsonconfig.yaml,并添加viper.SetConfigType("yaml")

1
2
3
4
viper.SetConfigName("config")
viper.AddConfigPath("./conf")
viper.SetConfigType("yaml")
viper.ReadInConfig()
  • Viper 会直接加载config.yaml忽略config.json(即使 json 优先级更高)。
  • config.yaml存在,viper.Get("port")将返回其值(如9090);若不存在,则报错(因强制指定 yaml 格式)。
  1. 注意事项
  • 文件扩展名与类型的关系
    若指定SetConfigType("yaml"),但文件扩展名为.json(如config.json),只要内容是 yaml 格式,Viper 仍会尝试解析,但可能引发混淆(建议保持扩展名与类型一致)。
  • 错误处理
    若指定类型与文件内容不匹配(如用SetConfigType("yaml")加载 json 文件),ReadInConfig()会抛出解析错误。

三、进阶:同时加载多个格式文件的正确方式

若需要合并多个格式的配置文件(如基础 json + 覆盖 yaml),需使用MergeConfigFromFile()而非ReadInConfig()

1
2
3
4
5
6
7
8
9
viper.SetConfigName("config")
viper.AddConfigPath("./conf")

// 先加载json基础配置
viper.SetConfigType("json")
viper.ReadInConfig()

// 再合并yaml覆盖配置
viper.MergeConfigFromFile("./conf/config.yaml")

此方式下,json 和 yaml 的配置会按顺序合并,后加载的 yaml 会覆盖 json 中相同的键。


四、总结

  1. 默认情况下:Viper 按["json", "yaml"]的优先级加载,先找到的config.json会被优先加载。
  2. 通过SetConfigType("yaml"):可强制 Viper 加载 yaml 格式文件,忽略扩展名优先级,直接解析指定类型的内容。
  3. 最佳实践:
    • 明确配置文件类型时,建议同时使用SetConfigType()和匹配的扩展名(如.yaml)。
    • 若需合并多格式配置,使用MergeConfigFromFile()按优先级手动合并。

如需进一步验证,可通过viper.AllSettings()打印实际加载的配置内容,确认来源文件。





3.3 写入配置文件

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

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

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

一个小示例:

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




3.4 监控并重新读取配置文件

Viper支持在运行时实时读取配置文件的功能。

需要重新启动服务器以使配置生效的日子已经一去不复返了,viper驱动的应用程序可以在运行时读取配置文件的更新,而不会错过任何消息。

只需告诉viper实例watchConfig。可选地,你可以为Viper提供一个回调函数,以便在每次发生更改时运行。

确保在调用WatchConfig()之前添加了所有的配置路径。

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

不需要热更的场景,就别开启了,尽管开销不高。





3.5 从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
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"




3.6 Viper 的配置覆盖

Viper 支持多层级的配置来源,并且后设置的配置会覆盖前面的配置。这一特性允许你在不同环境下灵活调整应用行为,而无需修改基础配置文件。

配置优先级层级 — Viper 的配置读取顺序(从高到低):

  1. 显式调用 viper.Set() 设置的值
  2. 命令行参数 (flags)
  3. 环境变量
  4. 配置文件
  5. 默认值




3.7 注册和使用别名

别名允许多个键引用单个值

1
2
3
4
5
6
7
viper.RegisterAlias("loud", "Verbose")  // 注册别名(此处loud和Verbose建立了别名)

viper.Set("verbose", true) // 结果与下一行相同
viper.Set("loud", true) // 结果与前一行相同

viper.GetBool("loud") // true
viper.GetBool("verbose") // true




3.8 使用环境变量

Viper完全支持环境变量。这使Twelve-Factor App开箱即用。有五种方法可以帮助与ENV协作:

  • AutomaticEnv()
  • BindEnv(string...) : error
  • SetEnvPrefix(string)
  • SetEnvKeyReplacer(string...) *strings.Replacer
  • AllowEmptyEnv(bool)

使用ENV变量时,务必要意识到Viper将ENV变量视为区分大小写。

Viper提供了一种机制来确保ENV变量是唯一的。通过使用SetEnvPrefix,你可以告诉Viper在读取环境变量时使用前缀。BindEnvAutomaticEnv都将使用这个前缀。

BindEnv使用一个或两个参数。第一个参数是键名称,第二个是环境变量的名称。环境变量的名称区分大小写。如果没有提供ENV变量名,那么Viper将自动假设ENV变量与以下格式匹配:前缀+ “_” +键名全部大写。当你显式提供ENV变量名(第二个参数)时,它 不会 自动添加前缀。例如,如果第二个参数是“id”,Viper将查找环境变量“ID”。

在使用ENV变量时,需要注意的一件重要事情是,每次访问该值时都将读取它。Viper在调用BindEnv时不固定该值。

AutomaticEnv是一个强大的助手,尤其是与SetEnvPrefix结合使用时。调用时,Viper会在发出viper.Get请求时随时检查环境变量。它将应用以下规则。它将检查环境变量的名称是否与键匹配(如果设置了EnvPrefix)。

SetEnvKeyReplacer允许你使用strings.Replacer对象在一定程度上重写 Env 键。如果你希望在Get()调用中使用-或者其他什么符号,但是环境变量里使用_分隔符,那么这个功能是非常有用的。可以在viper_test.go中找到它的使用示例。

或者,你可以使用带有NewWithOptions工厂函数的EnvKeyReplacer。与SetEnvKeyReplacer不同,它接受StringReplacer接口,允许你编写自定义字符串替换逻辑。

默认情况下,空环境变量被认为是未设置的,并将返回到下一个配置源。若要将空环境变量视为已设置,请使用AllowEmptyEnv方法。

Env 示例:

1
2
3
4
5
6
viper.SetEnvPrefix("spf") // 将自动转为大写
viper.BindEnv("id")

os.Setenv("SPF_ID", "13") // 通常是在应用程序之外完成的

id := viper.Get("id") // 13




3.9 使用Flags

Viper 具有绑定到标志的能力。具体来说,Viper支持Cobra库中使用的Pflag

BindEnv类似,该值不是在调用绑定方法时设置的,而是在访问该方法时设置的。这意味着你可以根据需要尽早进行绑定,即使在init()函数中也是如此。

对于单个标志,BindPFlag()方法提供此功能。

例如:

1
2
serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))

你还可以绑定一组现有的pflags (pflag.FlagSet):

举个例子:

1
2
3
4
5
6
pflag.Int("flagname", 1234, "help message for flagname")

pflag.Parse()
viper.BindPFlags(pflag.CommandLine)

i := viper.GetInt("flagname") // 从viper而不是从pflag检索值

在 Viper 中使用 pflag 并不阻碍其他包中使用标准库中的 flag 包。pflag 包可以通过导入这些 flags 来处理flag包定义的flags。这是通过调用pflag包提供的便利函数AddGoFlagSet()来实现的。

例如:

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

import (
"flag"
"github.com/spf13/pflag"
)

func main() {

// 使用标准库 "flag" 包
flag.Int("flagname", 1234, "help message for flagname")

pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
pflag.Parse()
viper.BindPFlags(pflag.CommandLine)

i := viper.GetInt("flagname") // 从 viper 检索值

...
}

flag接口

如果你不使用Pflag,Viper 提供了两个Go接口来绑定其他 flag 系统。

FlagValue表示单个flag。这是一个关于如何实现这个接口的非常简单的例子:

1
2
3
4
5
type myFlag struct {}
func (f myFlag) HasChanged() bool { return false }
func (f myFlag) Name() string { return "my-flag-name" }
func (f myFlag) ValueString() string { return "my-flag-value" }
func (f myFlag) ValueType() string { return "string" }

一旦你的 flag 实现了这个接口,你可以很方便地告诉Viper绑定它:

1
viper.BindFlagValue("my-flag-name", myFlag{})

FlagValueSet代表一组 flags 。这是一个关于如何实现这个接口的非常简单的例子:

1
2
3
4
5
6
7
8
9
type myFlagSet struct {
flags []myFlag
}

func (f myFlagSet) VisitAll(fn func(FlagValue)) {
for _, flag := range flags {
fn(flag)
}
}

一旦你的flag set实现了这个接口,你就可以很方便地告诉Viper绑定它:

1
2
3
4
fSet := myFlagSet{
flags: []myFlag{myFlag{}, myFlag{}},
}
viper.BindFlagValues("my-flags", fSet)




3.10 远程Key/Value存储支持

在Viper中启用远程支持,需要在代码中匿名导入viper/remote这个包。

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

Viper将读取从Key/Value存储(例如etcd或Consul)中的路径检索到的配置字符串(如JSONTOMLYAMLHCLenvfileJava properties格式)。这些值的优先级高于默认值,但是会被从磁盘、flag或环境变量检索到的配置值覆盖。(译注:也就是说Viper加载配置值的优先级为:磁盘上的配置文件>命令行标志位>环境变量>远程Key/Value存储>默认值。)

远程Key/Value存储示例-未加密

etcd

1
2
3
viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001","/config/hugo.json")
viper.SetConfigType("json") // 因为在字节流中没有文件扩展名,所以这里需要设置下类型。支持的扩展名有 "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv"
err := viper.ReadRemoteConfig()

Consul

你需要 Consul Key/Value存储中设置一个Key保存包含所需配置的JSON值。例如,创建一个keyMY_CONSUL_KEY将下面的值存入Consul key/value 存储:

1
2
3
4
5
6
7
8
9
10
{
"port": 8080,
"hostname": "liwenzhou.com"
}
viper.AddRemoteProvider("consul", "localhost:8500", "MY_CONSUL_KEY")
viper.SetConfigType("json") // 需要显示设置成json
err := viper.ReadRemoteConfig()

fmt.Println(viper.Get("port")) // 8080
fmt.Println(viper.Get("hostname")) // liwenzhou.com

Firestore

1
2
3
viper.AddRemoteProvider("firestore", "google-cloud-project-id", "collection/document")
viper.SetConfigType("json") // 配置的格式: "json", "toml", "yaml", "yml"
err := viper.ReadRemoteConfig()

当然,你也可以使用SecureRemoteProvider





3.11 监控etcd中的更改-未加密

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
// 或者你可以创建一个新的viper实例
var runtime_viper = viper.New()

runtime_viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001", "/config/hugo.yml")
runtime_viper.SetConfigType("yaml") // 因为在字节流中没有文件扩展名,所以这里需要设置下类型。支持的扩展名有 "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv"

// 第一次从远程读取配置
err := runtime_viper.ReadRemoteConfig()

// 反序列化
runtime_viper.Unmarshal(&runtime_conf)

// 开启一个单独的goroutine一直监控远端的变更
go func(){
for {
time.Sleep(time.Second * 5) // 每次请求后延迟一下

// 目前只测试了etcd支持
err := runtime_viper.WatchRemoteConfig()
if err != nil {
log.Errorf("unable to read remote config: %v", err)
continue
}

// 将新配置反序列化到我们运行时的配置结构体中。你还可以借助channel实现一个通知系统更改的信号
runtime_viper.Unmarshal(&runtime_conf)
}
}()




3.12 consul实践

  1. 准备本地配置文件

假设config-dev.yaml内容如下:

1
2
3
4
5
6
7
8
database:
host: localhost
port: 5432
username: admin
password: secret
api:
key: "123456"
timeout: 30s

  1. 上传consul

使用consul cli

1
2
❯ consul kv put config/config-dev.yaml @config-dev.yaml 
Success! Data written to: config/config-dev.yaml
  • @符号表示从文件读取内容
  • config/config-dev.yaml是 Consul 中的存储路径

下面这个方法,会导致配置文件格式乱掉,别用

1
2
❯ curl -X PUT -d @config-dev.yaml http://127.0.0.1:8500/v1/kv/config/config-dev.yaml
true%

在consul UI可以看到对应的内容:


  1. 代码实现
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
package main

import (
"crypto/md5"
"encoding/json"
"fmt"
"sync"
"time"

"github.com/gw-gong/gwkit-go/log"

"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
_ "github.com/spf13/viper/remote" // 导入远程配置支持
)

// 配置结构体
type Config struct {
Database struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
} `mapstructure:"database"`
API struct {
Key string `mapstructure:"key"`
Timeout time.Duration `mapstructure:"timeout"`
} `mapstructure:"api"`
}

var (
config Config
configMu sync.RWMutex
)

func main() {
syncFn, err := log.InitGlobalLogger(log.NewDefaultLoggerConfig())
if err != nil {
panic(err)
}
defer syncFn()

// 创建Viper实例
v := viper.New()

// 配置Consul远程提供者
v.AddRemoteProvider("consul", "127.0.0.1:8500", "config/config-dev.yaml")
v.SetConfigType("yaml") // 指定配置类型

// 首次读取配置
if err := v.ReadRemoteConfig(); err != nil {
log.Error("无法读取远程配置", log.Err(err))
return
}

// 解析配置到结构体
if err := v.Unmarshal(&config); err != nil {
log.Error("配置解析失败", log.Err(err))
return
}

log.Info("配置已加载", log.Any("config", config))

// 启动配置监控
go watchConfigChanges(v)

// 模拟使用配置
go func() {
for {
configMu.RLock()
log.Info("当前数据库配置", log.Str("host", config.Database.Host), log.Int("port", config.Database.Port))
configMu.RUnlock()
time.Sleep(5 * time.Second)
}
}()

// 保持主程序运行
select {}
}

// 监控配置变更
func watchConfigChanges(v *viper.Viper) {
ticker := time.NewTicker(10 * time.Second) // 增加间隔到10秒
defer ticker.Stop()

// 记录初始配置哈希
lastConfigHash := calculateConfigHash(v)
log.Info("初始配置哈希", log.Any("lastConfigHash", lastConfigHash))

for {
select {
case <-ticker.C:
// 尝试重新读取远程配置
if err := v.ReadRemoteConfig(); err != nil {
log.Error("读取远程配置失败", log.Err(err))
continue
}

// 计算新配置哈希
currentHash := calculateConfigHash(v)

// 比较哈希值,检测变化
if currentHash != lastConfigHash {
log.Info("检测到配置变化", log.Str("lastConfigHash", lastConfigHash), log.Str("currentHash", currentHash))
lastConfigHash = currentHash

// 手动触发配置变更回调
// 注意:这里我们手动触发,因为远程配置的监听可能不够可靠
var newConfig Config
if err := v.Unmarshal(&newConfig); err != nil {
log.Error("解析更新后的配置失败", log.Err(err))
continue
}

// 更新配置
configMu.Lock()
config = newConfig
configMu.Unlock()

log.Info("配置已手动更新", log.Any("config", config))
}
}
}
}

// 计算配置哈希,用于检测变化
func calculateConfigHash(v *viper.Viper) string {
// 获取所有配置设置
settings := v.AllSettings()

// 将配置转换为JSON字符串
configBytes, err := json.Marshal(settings)
if err != nil {
log.Error("序列化配置失败: %v", log.Err(err))
return ""
}

// 计算MD5哈希
hash := md5.Sum(configBytes)
return fmt.Sprintf("%x", hash)
}




四、从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")
}




4.1 访问嵌套的键

访问器方法也接受深度嵌套键的格式化路径。例如,如果加载下面的JSON文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"host": {
"address": "localhost",
"port": 5799
},
"datastore": {
"metric": {
"host": "127.0.0.1",
"port": 3099
},
"warehouse": {
"host": "198.0.0.1",
"port": 2112
}
}
}

Viper可以通过传入.分隔的路径来访问嵌套字段:

1
GetString("datastore.metric.host") // (返回 "127.0.0.1")

这遵守上面建立的优先规则;搜索路径将遍历其余配置注册表,直到找到为止。(译注:因为Viper支持从多种配置来源,例如磁盘上的配置文件>命令行标志位>环境变量>远程Key/Value存储>默认值,我们在查找一个配置的时候如果在当前配置源中没找到,就会继续从后续的配置源查找,直到找到为止。)

例如,在给定此配置文件的情况下,datastore.metric.hostdatastore.metric.port均已定义(并且可以被覆盖)。如果另外在默认值中定义了datastore.metric.protocol,Viper也会找到它。

然而,如果datastore.metric被直接赋值覆盖(被flag,环境变量,set()方法等等…),那么datastore.metric的所有子键都将变为未定义状态,它们被高优先级配置级别“遮蔽”(shadowed)了。

最后,如果存在与分隔的键路径匹配的键,则返回其值。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"datastore.metric.host": "0.0.0.0",
"host": {
"address": "localhost",
"port": 5799
},
"datastore": {
"metric": {
"host": "127.0.0.1",
"port": 3099
},
"warehouse": {
"host": "198.0.0.1",
"port": 2112
}
}
}

GetString("datastore.metric.host") // 返回 "0.0.0.0"




4.2 提取子树

从Viper中提取子树。

例如,viper实例现在代表了以下配置:

1
2
3
4
5
6
7
app:
cache1:
max-items: 100
item-size: 64
cache2:
max-items: 200
item-size: 80

执行后:

1
subv := viper.Sub("app.cache1")

subv现在就代表:

1
2
max-items: 100
item-size: 64

假设我们现在有这么一个函数:

1
func NewCache(cfg *Viper) *Cache {...}

它基于subv格式的配置信息创建缓存。现在,可以轻松地分别创建这两个缓存,如下所示:

1
2
3
4
5
cfg1 := viper.Sub("app.cache1")
cache1 := NewCache(cfg1)

cfg2 := viper.Sub("app.cache2")
cache2 := NewCache(cfg2)




4.3 反序列化

你还可以选择将所有或特定的值解析到结构体、map等。

有两种方法可以做到这一点:

  • Unmarshal(rawVal interface{}) : error
  • UnmarshalKey(key string, rawVal interface{}) : error

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
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
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
/*
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在后台使用github.com/mitchellh/mapstructure来解析值,其默认情况下使用mapstructuretag。

注意 当我们需要将viper读取的配置反序列到我们定义的结构体变量中时,一定要使用mapstructuretag哦





4.4 序列化成字符串

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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)
}




五、使用单个还是多个Viper实例?

Viper是开箱即用的。你不需要配置或初始化即可开始使用Viper。由于大多数应用程序都希望使用单个中央存储库管理它们的配置信息,所以viper包提供了这个功能。它类似于单例模式。

在上面的所有示例中,它们都以其单例风格的方法演示了如何使用viper。


使用多个viper实例

你还可以在应用程序中创建许多不同的viper实例。每个都有自己独特的一组配置和值。每个人都可以从不同的配置文件,key value存储区等读取数据。每个都可以从不同的配置文件、键值存储等中读取。viper包支持的所有功能都被镜像为viper实例的方法。

例如:

1
2
3
4
5
6
7
x := viper.New()
y := viper.New()

x.SetDefault("ContentDir", "content")
y.SetDefault("ContentDir", "foobar")

// ...

当使用多个viper实例时,由用户来管理不同的viper实例。





六、使用Viper示例

假设我们的项目现在有一个./conf/config.yaml配置文件,内容如下:

1
2
port: 8123
version: "v1.2.3"

接下来通过示例代码演示两种在项目中使用viper管理项目配置信息的方式。





6.1 直接使用viper管理配置

这里用一个demo演示如何在gin框架搭建的web项目中使用viper,使用viper加载配置文件中的信息,并在代码中直接使用viper.GetXXX()方法获取对应的配置值。

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"
"net/http"

"github.com/gin-gonic/gin"
"github.com/spf13/viper"
)

func main() {
viper.SetConfigFile("./conf/config.yaml") // 指定配置文件路径
err := viper.ReadInConfig() // 读取配置信息
if err != nil { // 读取配置信息失败
panic(fmt.Errorf("Fatal error config file: %s \n", err))
}

// 监控配置文件变化
viper.WatchConfig()

r := gin.Default()
// 访问/version的返回值会随配置文件的变化而变化
r.GET("/version", func(c *gin.Context) {
c.String(http.StatusOK, viper.GetString("version"))
})

if err := r.Run(
fmt.Sprintf(":%d", viper.GetInt("port"))); err != nil {
panic(err)
}
}




6.2 使用结构体变量保存配置信息

除了上面的用法外,我们还可以在项目中定义与配置文件对应的结构体,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
46
47
48
49
package main

import (
"fmt"
"net/http"

"github.com/fsnotify/fsnotify"

"github.com/gin-gonic/gin"
"github.com/spf13/viper"
)

type Config struct {
Port int `mapstructure:"port"`
Version string `mapstructure:"version"`
}

var Conf = new(Config)

func main() {
viper.SetConfigFile("./conf/config.yaml") // 指定配置文件路径
err := viper.ReadInConfig() // 读取配置信息
if err != nil { // 读取配置信息失败
panic(fmt.Errorf("Fatal error config file: %s \n", err))
}
// 将读取的配置信息保存至全局变量Conf
if err := viper.Unmarshal(Conf); err != nil {
panic(fmt.Errorf("unmarshal conf failed, err:%s \n", err))
}
// 监控配置文件变化
viper.WatchConfig()
// 注意!!!配置文件发生变化后要同步到全局变量Conf
viper.OnConfigChange(func(in fsnotify.Event) {
fmt.Println("夭寿啦~配置文件被人修改啦...")
if err := viper.Unmarshal(Conf); err != nil {
panic(fmt.Errorf("unmarshal conf failed, err:%s \n", err))
}
})

r := gin.Default()
// 访问/version的返回值会随配置文件的变化而变化
r.GET("/version", func(c *gin.Context) {
c.String(http.StatusOK, Conf.Version)
})

if err := r.Run(fmt.Sprintf(":%d", Conf.Port)); err != nil {
panic(err)
}
}




七、代码封装(😎)

在我的仓库 gwkit-go 中已经封装好了相关代码,在 /hot_cfg 目录下有封装代码,只要实现接口就可以使用,

使用例子在 /internal/examples/case003 有详细的演示,使用的时候只需要 copy 代码,稍加修改即可。


使用:

step 1: 自定义配置文件对应的结构体,并继承BaseConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
type Config struct {
*hot_cfg.BaseConfig
Database struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Username string `yaml:"username"`
Password string `yaml:"password"`
} `yaml:"database"`
API struct {
Key string `yaml:"key"`
Timeout time.Duration `yaml:"timeout"`
} `yaml:"api"`
}

step 2: 实现接口中的两个方法

1
2
3
4
5
6
7
8
9
type HotUpdate interface {
GetBaseConfig() *BaseConfig
AsLocalConfig() LocalConfig
AsConsulConfig() ConsulConfig

// Only need to implement these two methods, others inherit from BaseConfig
UnmarshalConfig() error
ReloadConfig()
}

step 3: 在用这个结构体做一个全局变量(单例模式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var (
Cfg *Config
once sync.Once
)

func InitConfig(filePath, fileName, fileType string) error {
var err error
once.Do(func() {
Cfg = &Config{}
Cfg.BaseConfig, err = hot_cfg.NewBaseConfig(
hot_cfg.WithLocalConfig(filePath, fileName, fileType),
)
if err != nil {
err = fmt.Errorf("init base config failed: %w", err)
}
})
return err
}

step 4: 在main函数中启用一下即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
// ...

hucm := hot_cfg.GetHotUpdateManager()

err = config.InitConfig("config", "config-dev.yaml", "yaml")
gwkit_common.ExitOnErr(context.Background(), err)
err = hucm.RegisterHotUpdateConfig(config.Cfg)
gwkit_common.ExitOnErr(context.Background(), err)

hucm.Watch()

// ...
}

例子:https://github.com/gw-gong/gwkit-go/blob/master/internal/examples/case003


ps:如果consul 启用了 ACL 的话,viper是不支持直接配置token的,有两个办法:

  • 可以在agent中配置好token
  • 可以使用环境变量,viper也调用了consul 的 SDK,直接设置好环境变量,consul会自动读取。




摘抄:Go语言配置管理神器——Viper中文教程