Viper是适用于Go应用程序的完整配置解决方案。它被设计用于在应用程序中工作,并且可以处理所有类型的配置需求和格式。
一、安装
1
| go get github.com/spf13/viper
|
二、介绍
2.1 什么是Viper?
Viper是适用于Go应用程序(包括Twelve-Factor App
)的完整配置解决方案。它被设计用于在应用程序中工作,并且可以处理所有类型的配置需求和格式。它支持以下特性:
- 设置默认值
- 从
JSON
、TOML
、YAML
、HCL
、envfile
和Java properties
格式的配置文件读取配置信息
- 实时监控和重新读取配置文件(可选)
- 从环境变量中读取
- 从远程配置系统(etcd或Consul)读取并监控配置变化
- 从命令行参数读取配置
- 从buffer读取配置
- 显式配置值
2.2 为什么选择Viper?
在构建现代应用程序时,你无需担心配置文件格式;你想要专注于构建出色的软件。Viper的出现就是为了在这方面帮助你的。
Viper能够为你执行下列操作:
- 查找、加载和反序列化
JSON
、TOML
、YAML
、HCL
、INI
、envfile
和Java properties
格式的配置文件。
- 提供一种机制为你的不同配置选项设置默认值。
- 提供一种机制来通过命令行参数覆盖指定选项的值。
- 提供别名系统,以便在不破坏现有代码的情况下轻松重命名参数。
- 当用户提供了与默认值相同的命令行或配置文件时,可以很容易地分辨出它们之间的区别。
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支持JSON
、TOML
、YAML
、HCL
、envfile
和Java 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")
viper.ReadInConfig()
|
Viper 会按照内置的文件格式优先级顺序查找并加载第一个存在的文件。具体规则如下:
- Viper 支持的格式优先级顺序
Viper 内部维护了一个格式优先级列表,顺序为:
1
| ["json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv"]
|
因此,当./conf
目录下同时存在config.json
和config.yaml
时:
- Viper 会先尝试加载
config.json
(因为json
在列表中优先级高于yaml
)。
- 若
config.json
存在,则直接加载并忽略config.yaml
;若不存在,才会继续查找config.yaml
。
- 验证示例
假设目录结构如下:
1 2 3
| ./conf/ ├── config.json // 内容:{"port": 8080} └── config.yaml // 内容:port: 9090
|
执行ReadInConfig()
后,viper.Get("port")
将返回8080
(来自 json 文件),因为 json 优先级更高。
二、通过 SetConfigType (“yaml”) 指定类型能否达到预期?
可以,且会覆盖默认的格式优先级逻辑。具体行为如下:
- SetConfigType 的作用
调用viper.SetConfigType("yaml")
后,Viper 会:
- 忽略文件扩展名,直接按指定的格式(yaml)解析内容。
- 强制查找名为
config
且格式为 yaml 的文件,即:
- 优先查找
config.yaml
或config.yml
(因扩展名匹配),
- 若不存在,也会尝试加载其他扩展名的文件(如
config.txt
),但要求文件内容符合 yaml 格式。
- 示例场景验证
当同时存在config.json
和config.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 格式)。
- 注意事项
- 文件扩展名与类型的关系:
若指定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")
viper.SetConfigType("json") viper.ReadInConfig()
viper.MergeConfigFromFile("./conf/config.yaml")
|
此方式下,json 和 yaml 的配置会按顺序合并,后加载的 yaml 会覆盖 json 中相同的键。
四、总结
- 默认情况下:Viper 按
["json", "yaml"]
的优先级加载,先找到的config.json
会被优先加载。
- 通过
SetConfigType("yaml")
:可强制 Viper 加载 yaml 格式文件,忽略扩展名优先级,直接解析指定类型的内容。
- 最佳实践:
- 明确配置文件类型时,建议同时使用
SetConfigType()
和匹配的扩展名(如.yaml
)。
- 若需合并多格式配置,使用
MergeConfigFromFile()
按优先级手动合并。
如需进一步验证,可通过viper.AllSettings()
打印实际加载的配置内容,确认来源文件。
3.3 写入配置文件
从配置文件中读取配置文件是有用的,但是有时你想要存储在运行时所做的所有修改。为此,可以使用下面一组命令,每个命令都有自己的用途:
- WriteConfig - 将当前的
viper
配置写入预定义的路径并覆盖(如果存在的话)。如果没有预定义的路径,则报错。
- SafeWriteConfig - 将当前的
viper
配置写入预定义的路径。如果没有预定义的路径,则报错。如果存在,将不会覆盖当前的配置文件。
- WriteConfigAs - 将当前的
viper
配置写入给定的文件路径。将覆盖给定的文件(如果它存在的话)。
- SafeWriteConfigAs - 将当前的
viper
配置写入给定的文件路径。不会覆盖给定的文件(如果它存在的话)。
根据经验,标记为safe
的所有方法都不会覆盖任何文件,而是直接创建(如果不存在),而默认行为是创建或截断。
一个小示例:
1 2 3 4 5
| viper.WriteConfig() 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")
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")
|
3.6 Viper 的配置覆盖
Viper 支持多层级的配置来源,并且后设置的配置会覆盖前面的配置。这一特性允许你在不同环境下灵活调整应用行为,而无需修改基础配置文件。
配置优先级层级 — Viper 的配置读取顺序(从高到低):
- 显式调用
viper.Set()
设置的值
- 命令行参数 (flags)
- 环境变量
- 配置文件
- 默认值
3.7 注册和使用别名
别名允许多个键引用单个值
1 2 3 4 5 6 7
| viper.RegisterAlias("loud", "Verbose")
viper.Set("verbose", true) viper.Set("loud", true)
viper.GetBool("loud") viper.GetBool("verbose")
|
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在读取环境变量时使用前缀。BindEnv
和AutomaticEnv
都将使用这个前缀。
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")
|
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 并不阻碍其他包中使用标准库中的 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.Int("flagname", 1234, "help message for flagname")
pflag.CommandLine.AddGoFlagSet(flag.CommandLine) pflag.Parse() viper.BindPFlags(pflag.CommandLine)
i := viper.GetInt("flagname")
... }
|
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)中的路径检索到的配置字符串(如JSON
、TOML
、YAML
、HCL
、envfile
和Java 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") 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") err := viper.ReadRemoteConfig()
fmt.Println(viper.Get("port")) fmt.Println(viper.Get("hostname"))
|
Firestore
1 2 3
| viper.AddRemoteProvider("firestore", "google-cloud-project-id", "collection/document") viper.SetConfigType("json") 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
| var runtime_viper = viper.New()
runtime_viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001", "/config/hugo.yml") runtime_viper.SetConfigType("yaml")
err := runtime_viper.ReadRemoteConfig()
runtime_viper.Unmarshal(&runtime_conf)
go func(){ for { time.Sleep(time.Second * 5)
err := runtime_viper.WatchRemoteConfig() if err != nil { log.Errorf("unable to read remote config: %v", err) continue }
runtime_viper.Unmarshal(&runtime_conf) } }()
|
3.12 consul实践
- 准备本地配置文件
假设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
|
- 上传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 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()
v := viper.New()
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) 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()
configBytes, err := json.Marshal(settings) if err != nil { log.Error("序列化配置失败: %v", log.Err(err)) return "" }
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")
|
这遵守上面建立的优先规则;搜索路径将遍历其余配置注册表,直到找到为止。(译注:因为Viper支持从多种配置来源,例如磁盘上的配置文件>命令行标志位>环境变量>远程Key/Value存储>默认值,我们在查找一个配置的时候如果在当前配置源中没找到,就会继续从后续的配置源查找,直到找到为止。)
例如,在给定此配置文件的情况下,datastore.metric.host
和datastore.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")
|
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
|
type config struct { Module struct { Enabled bool
moduleConfig `mapstructure:",squash"` } }
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来解析值,其默认情况下使用mapstructure
tag。
注意 当我们需要将viper读取的配置反序列到我们定义的结构体变量中时,一定要使用mapstructure
tag哦!
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() 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)) } if err := viper.Unmarshal(Conf); err != nil { panic(fmt.Errorf("unmarshal conf failed, err:%s \n", err)) } viper.WatchConfig() 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() 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
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中文教程