在 Go 中如何使用 Viper 来管理配置 2

简介: 在 Go 中如何使用 Viper 来管理配置

从 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:jianghushinian
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
package main
import (
"fmt"
"github.com/spf13/viper"
)
funcmain() {
	viper.SetConfigFile("./config.yaml")
	viper.ReadInConfig()
// 读取配置值
	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: jianghushinian
server: map[ip:127.0.0.1 port:8080]
server.ip: 10.0.0.1
server.port: 8080

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

现在 config.yaml 配置如下:

1
2
3
4
5
6
username:jianghushinian
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
package main
import (
"fmt"
"github.com/spf13/viper"
)
funcmain() {
	viper.SetConfigFile("./config.yaml")
	viper.ReadInConfig()
// 读取配置值
	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: jianghushinian
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
package main
import (
"fmt"
"github.com/spf13/viper"
)
funcmain() {
	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"))
}

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

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

反序列化

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
package main
import (
"fmt"
"github.com/spf13/viper"
)
type Config struct {
	Username string
	Password string
// Viper 支持嵌套结构体
	Server struct {
		IP   string
		Port int
	}
}
funcmain() {
	viper.SetConfigFile("./config.yaml")
	viper.ReadInConfig()
var cfg *Config
if err := viper.Unmarshal(&cfg); err != nil {
panic(err)
	}
var password *string
if err := viper.UnmarshalKey("password", &password); err != nil {
panic(err)
	}
	fmt.Printf("cfg: %+v\n", cfg)
	fmt.Printf("password: %s\n", *password)
}

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

1
2
3
$ go run main.go 
cfg: &{Username:jianghushinian Password:123456 Server:{IP:127.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{}
	}
}
funcmain() {
// 默认的键分隔符为 `.`,这里将其修改为 `::`
	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
$ 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 来解决。

序列化

一个好用的配置包不仅能够支持反序列化操作,还要支持序列化操作。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"
	yaml "gopkg.in/yaml.v2"
)
// 序列化配置为 YAML 格式字符串
funcyamlStringSettings()string {
	c := viper.AllSettings() // 获取全部配置
	bs, _ := yaml.Marshal(c) // 根据需求序列化成不同格式
returnstring(bs)
}
funcmain() {
	viper.SetConfigFile("./config.yaml")
	viper.ReadInConfig()
	fmt.Printf(yamlStringSettings())
}

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

1
2
3
4
5
6
$ go run main.go
password: 123456
server:
  ip: 127.0.0.1
  port: 8080
username: jianghushinian
写入配置文件

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

  • WriteConfig:将当前的 viper 配置写入预定义路径。如果没有预定义路径,则会报错。如果预定义路径已经存在配置文件,将会被覆盖。
  • SafeWriteConfig:将当前的 viper 配置写入预定义路径。如果没有预定义路径,则会报错。如果预定义路径已经存在配置文件,不会覆盖,会报错。
  • WriteConfigAs: 将当前的 viper 配置写入给定的文件路径。如果给定的文件路径已经存在配置文件,将会被覆盖。
  • SafeWriteConfigAs:将当前的 viper 配置写入给定的文件路径。如果给定的文件路径已经存在配置文件,不会覆盖,会报错。

使用示例:

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

多实例对象

由于大多数应用程序都希望使用单个配置实例对象来管理配置,因此 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
package main
import (
"fmt"
"github.com/spf13/viper"
)
funcmain() {
	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 实例 xy,它们分别从配置文件读取配置和通过默认值的方式设置配置,使用时互不影响,使用者可以自行管理它们的生命周期。

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

1
2
3
$ go run main.go
x.username: jianghushinian
y.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"
)
funcmain() {
	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
	}
}
funcmain() {
	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 版本的文章与读者一起学习进步。

联系我

参考

相关文章
|
6天前
|
前端开发 JavaScript Linux
Sublime Text 3配置Go语言开发环境
【4月更文挑战第13天】本篇文章 Huazie 向大家介绍使用 Sublime Text 3搭建Go语言开发环境,并演示编译运行 Go语言代码
36 8
Sublime Text 3配置Go语言开发环境
|
24天前
|
Go 开发者
掌握Go语言:Go语言结构体,精准封装数据,高效管理实体对象(22)
掌握Go语言:Go语言结构体,精准封装数据,高效管理实体对象(22)
|
3月前
|
关系型数据库 MySQL Go
工厂模式+自动注册管理Go多包结构体
工厂模式+自动注册管理Go多包结构体
45 1
|
6月前
|
测试技术 Go 数据安全/隐私保护
Go语言包管理不再痛,包引用问题彻底解决
Go语言包管理不再痛,包引用问题彻底解决
36 0
|
6月前
|
Go
Go语言构造函数的力量:初始化和配置的艺术
Go语言构造函数的力量:初始化和配置的艺术
45 0
|
6月前
|
Web App开发 存储 IDE
手把手教你使用LiteIDE配置Go语言开发环境
手把手教你使用LiteIDE配置Go语言开发环境
149 0
|
29天前
|
数据建模 Go vr&ar
idea配置go依赖下载
idea配置go依赖下载
16 0
|
4月前
|
Go
go语言中的配置管理神器 --viper 二 实战
go语言中的配置管理神器 --viper 二 实战
83 0
|
4月前
|
存储 缓存 NoSQL
go语言中的配置管理神器 --viper 一(二)
go语言中的配置管理神器 --viper 一
59 0
|
4月前
|
存储 JSON Java
go语言中的配置管理神器 --viper 一(一)
go语言中的配置管理神器 --viper 一
52 0