当前位置:网站首页>Go项目配置管理神器之viper使用详解
Go项目配置管理神器之viper使用详解
2022-08-10 23:53:00 【CK持续成长】
目录
1. viper的介绍
viper是go一个强大的流行的配置解决方案的库。viper是spf13的另外一个重量级库。有大量项目都使用该库,比如hugo, docker等。 它基本上可以处理所有类型的配置需求和格式, viper支持功能
- 设置默认配置
- 支持各种配置文件,如JSON,TOML, YAML, HCL, envfile和Java属性配置文件
- 支持监听文件变化以及重新读取配置
- 支持从环境变量读取配置
- 支持从远程配置系统(etcd或Consul)读取配置,并能监听远程配置修改
- 支持从命令行标志Flag读取配置,比如搭配cobra使用
- 支持读取缓冲区数据
Viper主要为我们做以下工作:
- 查找、加载和解组JSON、TOML、YAML、HCL、INI、envfile或Java属性格式的配置文件。
- 提供一种机制来为不同的配置选项设置默认值。
- 提供一种机制来为通过命令行标志指定的选项设置覆盖值。
- 提供别名系统,以便在不破坏现有代码的情况下轻松重命名参数。
- 当用户提供了与默认值相同的命令行或配置文件时,很容易区分它们。
viepr的安装很简单,直接再工程中使用go get命令安装即可
$ go get github.com/spf13/viper
2. viper的使用
2.1 Viper对象的创建
Viper的是viper库的主要实现对象, viper提供了下面的方法可以获取Viper实例:
func GetViper() *Viper
func New() *Viper
func NewWithOptions(opts ...Option) *Viper
func Sub(key string) *Viper
- 使用viper.GetViper()获取的为全局的Viper实例对象,默认使用viper包使用也是该全局Viper实例。查看viper的源码,可以看到viper默认提供了一个全局的Viper实例:
var v *Viper
func init() {
v = New()
}
// New returns an initialized Viper instance.
func New() *Viper {
v := new(Viper)
v.keyDelim = "."
v.configName = "config"
v.configPermissions = os.FileMode(0o644)
v.fs = afero.NewOsFs()
v.config = make(map[string]interface{})
v.override = make(map[string]interface{})
v.defaults = make(map[string]interface{})
v.kvstore = make(map[string]interface{})
v.pflags = make(map[string]FlagValue)
v.env = make(map[string][]string)
v.aliases = make(map[string]string)
v.typeByDefValue = false
v.logger = jwwLogger{}
v.resetEncoding()
return v
}
- New和NewWithOptions为我们提供了创建实例的方法
func New1() *viper.Viper {
return viper.New()
}
func New2() *viper.Viper {
return viper.NewWithOptions()
}
- Sub为我们读取子配置项提供了一个新的实例Viper
v := viper.Sub("db")
url := v.Get("url")
log.Printf("mysql url:%s\n", url)
2.2 预设一些默认配置
viper.SetDefault("ContentDir", "content")
viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "categories"})
viper.SetDefault("redis.port", 6379)
viper.SetDefault("mysql.url", "root:[email protected](127.0.0.1:3306)/stock?charset=utf8mb4&parseTime=True&loc=Local")
2.3 从命令行工具的选项参数Flags读取
viper主要提供了以下四个方法,可以绑定行参数的输出的选项值:
func (v *Viper) BindFlagValue(key string, flag FlagValue) error
func (v *Viper) BindFlagValues(flags FlagValueSet) (err error)
func (v *Viper) BindPFlag(key string, flag *pflag.Flag) error
func (v *Viper) BindPFlags(flags *pflag.FlagSet) error
这里我们主要结合之前讲的cobra库中的pflag来讲解一下viper对Flags选项参数的绑定。
在cobra中,我们主要通过cobra.Command来组织不同的命令和子命令,这里我们我通过在root根命令来做测试。代码如下:
func init(){
rootCmd.Flags().String("author", "YOUR NAME", "Author name for copyright attribution")
rootCmd.Flags().String("email", "YOUR EMAIL", "Author email for contact")
// 绑定多个key-value值
viper.BindPFlags(rootCmd.Flags())
// 单个绑定不同的key
viper.BindPFlag("author", rootCmd.Flags().Lookup("author"))
viper.BindPFlag("email", rootCmd.Flags().Lookup("email"))
rootCmd.AddCommand(version.VersionCmd)
}
在cobra的命令的run回调方法中,我们通过viper的来获取输入的选项值
func run(){
fmt.Println("go root cmd run")
fmt.Println(viper.GetString("author"))
fmt.Println(viper.GetString("email"))
}
启动饮用,传入参数测试一下:
go run main.go --author ckeen --email [email protected]
查看一下打印结果,可以看到从viper成功获取到以flag传入的参数值:
* cli git:(master) * go run main.go --author keen --email [email protected]
go root cmd run
ckeen
[email protected]
2.4 从环境变量读取
viper支持环境变量的函数:
func (v *Viper) AutomaticEnv() // 开启绑定环境变量
func (v *Viper) BindEnv(input ...string) error // 绑定系统中某个环境变量
func (v *Viper) SetEnvKeyReplacer(r *strings.Replacer)
func (v *Viper) SetEnvPrefix(in string)
使用方法:
- 使用AutomaticEnv()开启绑定环境变量,没开启的时候不会从环境变量获取,开启后可以获取环境变量的值。如果不想开启所有环境变量值,可以使用BindEnv(input …string)方法绑定单个环境变量的绑定, 那么只有该绑定的环境变量的key才能获取到值
- 绑定环境变量后,可以使用正常的Get方法来获取变量值,示例代码如下:
func testEnv(){ v := New1() os.Setenv("CK_HOME","123") os.Setenv("CK_NAME","ckeen") v.AutomaticEnv() //v.BindEnv("SHELL") v.AllowEmptyEnv(true) log.Printf("os env:%+v\n", os.Environ()) log.Printf("env: %+v\n", v.Get("HOME")) log.Printf("env: %+v\n", v.Get("SHELL")) v.SetEnvPrefix("CK") log.Printf("ck-home: %+v\n", v.Get("HOME")) log.Printf("ck-email: %+v\n", v.Get("NAME")) } - 还可以通过SetEnvPrefix()方法设置环境变量前缀, 前缀和Key之间用下划线分割
2.5 从配置文件读取
下面我们看一下操作实例, 先看我们的配置文件app.yml文件:
app:
name: viper-test
mode: dev
db:
mysql:
url: "root:[email protected](127.0.0.1:3306)/stock?charset=utf8mb4&parseTime=True&loc=Local"
redis:
host: 127.0.0.1
port: 6067
db: 0
passwd: 123456- 初始化配置
func InitConfig() (*viper.Viper, error) { v := viper.New() v.AddConfigPath(".") // 添加配置文件搜索路径,点号为当前目录 v.AddConfigPath("./configs") // 添加多个搜索目录 v.SetConfigType("yaml") // 如果配置文件没有后缀,可以不用配置 v.SetConfigName("app.yml") // 文件名,没有后缀 // v.SetConfigFile("configs/app.yml") // 读取配置文件 if err := v.ReadInConfig(); err == nil { log.Printf("use config file -> %s\n", v.ConfigFileUsed()) } else { return nil,err } return v, nil }首先这里我们添加一个配置文件搜索路径,点号表示当前路径,搜索路径可以添加多个然后设置了配置文件类型,这里我们设置文件类型为yaml,
接着我们设置了配置文件名称,这个文件可以从配置的搜索路径从查找。
最后我们通过提供的ReadInConfig()函数读取配置文件
- 读取配置文件
// 通过.号来区分不同层级,来获取配置值 log.Printf("app.mode=%s\n", v.Get("app.mode")) log.Printf("db.mysql.url=%s\n", v.Get("db.mysql.url")) log.Printf("db.redis.host=%s\n", v.GetString("db.redis.host")) log.Printf("db.redis.port=%d\n", v.GetInt("db.redis.port")) // 使用Sub获取子配置,然后获取配置值 v2 := v.Sub("db") log.Printf("db.mysql.url:%s\n", v2.Sub("mysql").GetString("url")) log.Printf("db.redis.host:%s\n", v2.Sub("redis").GetString("host")) log.Printf("db.redis.port:%s\n", v2.Sub("redis").GetInt("port"))viper还提供了如下获取类型获取配置项值:

注: 其中重要的一个函数IsSet可以用来判断某个key是否被设置
2.6 从远程key/value存储读取
在Viper中启用远程支持,需要在代码中匿名导入viper/remote这个包。
_ "github.com/spf13/viper/remote"Viper将读取从Key/Value存储中的路径检索到的配置字符串(如JSON、TOML、YAML格式)。viper目前支持Consul/Etcd/firestore三种Key/Value的存储系统。下面我来演示从etcd读取配置:
- 首先我们安装crypt的工具
go get github.com/bketelsen/crypt/bin/crypt - 使用crypt的命令,将app.yml的文件添加到detcd
crypt set --endpoint=http://127.0.0.1:2379 -plaintext /config/app.yml /Users/ckeen/Documents/code/gosource/go-awesome/go-samples/viper/configs/app.yml - 添加viper的操作远程资源的配置
_ "github.com/spf13/viper/remote" - 实现从远程读取配置
func InitConfigFromRemote() (*viper.Viper,error) { v := viper.New() // 远程配置 v.AddRemoteProvider("etcd","http://127.0.0.1:2379","config/app.yml") //v.SetConfigType("json") v.SetConfigFile("app.yml") v.SetConfigType("yml") if err := v.ReadRemoteConfig(); err == nil { log.Printf("use config file -> %s\n", v.ConfigFileUsed()) } else { return nil, err } return v, nil } func main(){ v, err := InitConfigFromRemote() if err != nil { log.Printf("read remote error:%+v\n") } log.Printf("remote read app.mode=%+v\n", v.GetString("app.mode")) log.Printf("remote read db.mysql.url=%+v\n", v.GetString("db.mysql.url")) } - 测试打印结果

2.7 监听配置变化
viper提供如下两种监听配置的函数,一个是本地的监听和一个远程监听的:
func (v *Viper) WatchConfig()
func (v *Viper) WatchRemoteConfig() error
func (v *Viper) WatchRemoteConfigOnChannel() error
我们主要看一下监听本地文件变更的示例
v, err := InitConfig()
if err != nil {
log.Fatalf("viper读取失败, error:%+v\n",err)
}
// 监听到文件变化后的回调
v.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("Config file changed:", e.Name)
fmt.Println(v.Get("db.redis.passwd"))
})
v.WatchConfig()
// 阻塞进程退出
time.Sleep(time.Duration(1000000) * time.Second)
我们使用前面的InitConfig()方法来初始化本地文件读取配置,然后设定了监听函数,最后使用WatchConfig()开启本地文件监听。
当我们修改本地配置configs/app.yml的db.redis.passwd的值,然后保存后,我们可以看到控制台有打印最新修改后的值,不要我们重新去获取。
2.8 写入配置到文件
viper提供了如下四个写入配置文件发方法
func (v *Viper) SafeWriteConfig() error
func (v *Viper) SafeWriteConfigAs(filename string) error
func (v *Viper) WriteConfig() error
func (v *Viper) WriteConfigAs(filename string) error使用SafeWriteConfig()和WriteConfig()时,可以先设定SetConfigFile()设定配置文件的路径。配置写入示例:
v := New1()
v.SetConfigFile("./hello.yml")
log.Printf("config path:%+v\n", v.ConfigFileUsed())
v.SetDefault("author","CKeen")
v.SetDefault("email", "[email protected]")
v.Set("hello", "foo")
v.Set("slice", []string {"slice1","slice2","slice3"})
v.SetDefault("test.web", "https://ckeen.cn")
v.WriteConfig()
//v.WriteConfigAs("./hello.yml")如果使用SafeWriteConfigAs()或者WriteConfigAs()方法,则直接传入配置文件路径即可。
3. 源码分析--配置读取的顺序
通过上面的示例我们知道,viper读取配置主要通过一系列Get方法来实现,我们从Get方法跳转到源码可以发现, 主要获取的配置值的为find方法, 方法实现如下:
func (v *Viper) find(lcaseKey string, flagDefault bool) interface{} {
var (
val interface{}
exists bool
path = strings.Split(lcaseKey, v.keyDelim)
nested = len(path) > 1
)
// compute the path through the nested maps to the nested value
if nested && v.isPathShadowedInDeepMap(path, castMapStringToMapInterface(v.aliases)) != "" {
return nil
}
// if the requested key is an alias, then return the proper key
lcaseKey = v.realKey(lcaseKey)
path = strings.Split(lcaseKey, v.keyDelim)
nested = len(path) > 1
// Set() override first
val = v.searchMap(v.override, path)
if val != nil {
return val
}
if nested && v.isPathShadowedInDeepMap(path, v.override) != "" {
return nil
}
// PFlag override next
flag, exists := v.pflags[lcaseKey]
if exists && flag.HasChanged() {
switch flag.ValueType() {
case "int", "int8", "int16", "int32", "int64":
return cast.ToInt(flag.ValueString())
case "bool":
return cast.ToBool(flag.ValueString())
case "stringSlice", "stringArray":
s := strings.TrimPrefix(flag.ValueString(), "[")
s = strings.TrimSuffix(s, "]")
res, _ := readAsCSV(s)
return res
case "intSlice":
s := strings.TrimPrefix(flag.ValueString(), "[")
s = strings.TrimSuffix(s, "]")
res, _ := readAsCSV(s)
return cast.ToIntSlice(res)
case "stringToString":
return stringToStringConv(flag.ValueString())
default:
return flag.ValueString()
}
}
if nested && v.isPathShadowedInFlatMap(path, v.pflags) != "" {
return nil
}
// Env override next
if v.automaticEnvApplied {
// even if it hasn't been registered, if automaticEnv is used,
// check any Get request
if val, ok := v.getEnv(v.mergeWithEnvPrefix(lcaseKey)); ok {
return val
}
if nested && v.isPathShadowedInAutoEnv(path) != "" {
return nil
}
}
envkeys, exists := v.env[lcaseKey]
if exists {
for _, envkey := range envkeys {
if val, ok := v.getEnv(envkey); ok {
return val
}
}
}
if nested && v.isPathShadowedInFlatMap(path, v.env) != "" {
return nil
}
// Config file next
val = v.searchIndexableWithPathPrefixes(v.config, path)
if val != nil {
return val
}
if nested && v.isPathShadowedInDeepMap(path, v.config) != "" {
return nil
}
// K/V store next
val = v.searchMap(v.kvstore, path)
if val != nil {
return val
}
if nested && v.isPathShadowedInDeepMap(path, v.kvstore) != "" {
return nil
}
// Default next
val = v.searchMap(v.defaults, path)
if val != nil {
return val
}
if nested && v.isPathShadowedInDeepMap(path, v.defaults) != "" {
return nil
}
if flagDefault {
// last chance: if no value is found and a flag does exist for the key,
// get the flag's default value even if the flag's value has not been set.
if flag, exists := v.pflags[lcaseKey]; exists {
switch flag.ValueType() {
case "int", "int8", "int16", "int32", "int64":
return cast.ToInt(flag.ValueString())
case "bool":
return cast.ToBool(flag.ValueString())
case "stringSlice", "stringArray":
s := strings.TrimPrefix(flag.ValueString(), "[")
s = strings.TrimSuffix(s, "]")
res, _ := readAsCSV(s)
return res
case "intSlice":
s := strings.TrimPrefix(flag.ValueString(), "[")
s = strings.TrimSuffix(s, "]")
res, _ := readAsCSV(s)
return cast.ToIntSlice(res)
case "stringToString":
return stringToStringConv(flag.ValueString())
default:
return flag.ValueString()
}
}
// last item, no need to check shadowing
}
return nil
}通过源码,我们可以知道viper读取配置的优先级顺序:alias别名 > 调用Set设置 > flag > env > config > key/value store > default
还有一个注意点:viper配置键不区分大小写,因为viper内部对key统一转为了小写。
4. 参考资料
viper的包地址:viper package - github.com/spf13/viper - Go Packages
viper的github地址: GitHub - spf13/viper: Go configuration with fangs
边栏推荐
- promise详解
- 翻译软件哪个准确度高【免费】
- 鲲鹏编译调试及原生开发工具基础知识
- sqlmap combined with dnslog fast injection
- What is the ASIO4ALL
- Ali P7 bask in January payroll: hard to fill the, really sweet...
- 12. 处理 JSON
- In 22 years, the salary of programmers nationwide in January was released, only to know that there are so many with annual salary of more than 400,000?
- Pagoda Test-Building PHP Online Mock Exam System
- [C language articles] Expression evaluation (implicit type conversion, arithmetic conversion)
猜你喜欢
![[Excel knowledge and skills] Convert numeric format numbers to text format](/img/fb/79d6928456f090d47f0fe7a5074979.png)
[Excel knowledge and skills] Convert numeric format numbers to text format

10. Notes on receiving parameters

web 性能提升(将持续更新……)

【C语言篇】操作符之 位运算符详解(“ << ”,“ >> ”,“ & ”,“ | ”,“ ^ ”,“ ~ ”)

2. Dependency management and automatic configuration

How to recover data from accidentally deleted U disk, how to recover deleted data from U disk

【C语言】C语言程序设计:动态通讯录(顺序表实现)

7. yaml
![[Excel knowledge and skills] Convert](/img/96/ece9c3885fd4abe4bf4d211813b9c4.png)
[Excel knowledge and skills] Convert "false" date to "true" date format

英文文献阅读时,如何做笔记?
随机推荐
“蔚来杯“2022牛客暑期多校训练营3 DF题解
How to quickly grasp industry opportunities and introduce new ones more efficiently is an important proposition
ROS Experiment Notes - Validation of UZH-FPV Dataset
2022下半年软考「高项」易混淆知识点汇总(2)
深度学习 Transformer架构解析
Deep Learning Transformer Architecture Analysis
编程语言为什么有变量类型这个概念?
Excel English automatic translation into Chinese tutorial
2. Dependency management and automatic configuration
Design and Realization of Employment Management System in Colleges and Universities
C语言%(%d,%c...)
8. WEB 开发-静态资源访问
YOLOv5的Tricks | 【Trick11】在线模型训练可视化工具wandb(Weights & Biases)
如何便捷获取参考文献的引用格式?
只会懒汉式和饿汉式 你还不懂单例模式!
虚拟电厂可视化大屏,深挖痛点精准减碳
Easy-to-use translation plug-in - one-click automatic translation plug-in software
[21-day learning challenge - kernel notes] (5) - devmem read and write register debugging
如何判断一个数为多少进制?
ASIO4ALL是什么