在 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 版本的文章与读者一起学习进步。

联系我

参考

相关文章
|
4月前
|
算法 Java Go
运行时管理GO与Java的概要对比
【5月更文挑战第17天】本文介绍Go、Python和Java的运行时机制各异。Go是编译型语言,其runtime负责内存管理、GC和协程调度,强调性能和低延迟。Java的JVM兼顾跨平台和性能,使用字节码和JIT编译,其GC策略复杂且高效。三种语言在设计和优化上各有侧重,适用不同场景。
64 3
|
1月前
|
JSON 缓存 监控
go语言后端开发学习(五)——如何在项目中使用Viper来配置环境
Viper 是一个强大的 Go 语言配置管理库,适用于各类应用,包括 Twelve-Factor Apps。相比仅支持 `.ini` 格式的 `go-ini`,Viper 支持更多配置格式如 JSON、TOML、YAML
go语言后端开发学习(五)——如何在项目中使用Viper来配置环境
|
1月前
|
Linux Go
Linux——windows10下的Ubuntu18.04安装并配置go环境
Linux——windows10下的Ubuntu18.04安装并配置go环境
35 1
|
20天前
|
Unix 编译器 Go
|
2月前
|
JSON Go API
go项目实现通过配置文件进行配置项统一管理
go项目实现通过配置文件进行配置项统一管理
19 0
|
3月前
|
Go
go配置镜像(阿里云、七牛)
go配置镜像(阿里云、七牛)
162 1
|
2月前
|
Go 持续交付
使用 Makefile 管理和部署 Go 项目
在软件开发中,`Makefile` 用于自动化任务,提升效率。在Go项目中,它简化构建和部署。`Makefile`集成了编译、打包、清理和部署等任务,减少错误,提高效率。通过定义规则和依赖,`make`工具执行任务。示例展示了如何创建`Makefile`进行Go应用的自动化部署,包括构建、传输、停启服务。通过`make deploy-dev`一键执行部署流程。`Makefile`不仅简化部署,还可扩展实现更多复杂自动化,提升开发流程的专业性和效率。
39 0
|
3月前
|
Go
Go环境配置
【6月更文挑战第11天】
69 0
|
4月前
|
Go
配置go语言下载包 - 蓝易云
这个命令会将包下载到你的GOPATH目录下,并自动安装它。
89 1
|
4月前
|
IDE Go 开发工具
【GO基础】2. IDEA配置Go语言开发环境
【GO基础】2. IDEA配置Go语言开发环境
344 2