Viper配置库的使用

警告
本文最后更新于 2023-03-04 18:56,文中内容可能已过时。

一、简介

viper支持功能

  • 设置默认值
  • JSON, TOML, YAML, HCL, envfile, Java properties中读取配置
  • 实时监听和自动重载配置文件(可选功能)
  • 从环境变量读取配置
  • 从远程配置系统(etcd 或 Consul)读取,并监听和自动重载
  • 从命令行flag读取
  • 从缓冲区读取
  • 代码中手动Set配置
  • 参数别名系统
  • 支持查找多个路径为配置文件目录

注意: viper中的配置项不区分大小写

优先级

获取参数的优先级,从上到下,从高到低

  • Set() 函数设置的参数,优先级最高
  • flag 命令行的参数
  • env 环境变量的参数
  • config 配置文件的参数
  • key/value store 远程配置
  • default 默认值

二、安装

go get github.com/spf13/viper

三、常见用法

1. 设置默认值

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

2. 从文件读取

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
viper.SetConfigFile("app.yaml") // 配置文件路径,无扩展名的配置文件用这个读比较好
viper.SetConfigName("config") // 配置文件的名称, 不要带扩展名
viper.SetConfigType("yaml") // 如果文件名称中没有扩展名,则需要指定配置文件的格式
viper.AddConfigPath("/etc/appname/")   // 在/etc/appname/路径中查找配置文件
viper.AddConfigPath("$HOME/.appname")  // 多次调用,添加多个查找路径
viper.AddConfigPath(".")               // 多次调用,添加多个查找路径
err := viper.ReadInConfig() // 查找并读取配置文件
if err := viper.ReadInConfig(); err != nil {
	if _, ok := err.(viper.ConfigFileNotFoundError); ok {
		// 找不到配置文件
	} else {
		// 发生了其他的错误
	}
}

var SupportedExts = []string{“json”, “toml”, “yaml”, “yml”, “properties”, “props”, “prop”, “hcl”, “tfvars”, “dotenv”, “env”, “ini”}

var SupportedRemoteProviders = []string{“etcd”, “etcd3”, “consul”, “firestore”}

3. 设置和覆盖参数

1
viper.Set("Key", "Value")

4. 配置合并和将viper的配置持久化到本地

  • WriteConfig将当前的 viper 配置写入到(最后一次读取到的配置文件名+ConfiogType为后缀),覆盖写入。
  • SafeWriteConfig将当前的 viper 配置写入到(最后一次读取到的配置文件名+ConfiogType为后缀),如果文件存在,返回error。
  • WriteConfigAs 将当前的 viper 配置写入指定的文件路径,覆盖写入。(只能指定支持的后缀名)
  • SafeWriteConfigAs将当前的 viper 配置写入指定的文件路径,如果文件存在,则返回err。(只能指定支持的后缀名)
  • 获取最后一次读取的配置文件fmt.Println(viper.GetViper().ConfigFileUsed())
 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
package main

import (
	"fmt"

	"github.com/spf13/viper"
)

func main() {

	/*
		1. 设置配置文件目录
	*/
	viper.AddConfigPath("./config")

	/*
		2. 读取第一个配置文件app.yaml
		配置内容为
		default:
		  filename: app.yaml
		  type: yaml
	*/
	viper.SetConfigName("app")
	viper.SetConfigType("yaml")

	if err := viper.ReadInConfig(); err != nil {
		fmt.Print(err.Error())
	}
	fmt.Printf("只读取app.yaml:%+v \n", viper.AllSettings())

	// /*
	// 	3. 添加一个默认配置
	// */
	viper.SetDefault("default.name", "代码设置的值")

	/*
		4. 读取第二个配置文件.env.ini
		配置内容为
		type=ini
		filename=.env.ini
	*/
	viper.SetConfigName(".env")
	viper.SetConfigType("ini")
	// !!!!!合并配置, 后读取到的参数会覆盖之前的
	// 正常是会覆盖的,但是某些特殊情况不会覆盖,比如我测试yaml内容如果为default.type: yaml, ini的内容为type=ini, 这个情况就没有覆盖为ini
	if err := viper.MergeInConfig(); err != nil {
		fmt.Println("合并配置出错", err.Error())
	}
	fmt.Printf("读取.env.ini后:%+v \n", viper.AllSettings())

	/*
		5. 持久化配置到本地
		将 name="代码设置的值" 保存到本地配置文件中
	*/
	// 获取保存到哪个文件
	fmt.Println("目标保存配置文件路径", viper.GetViper().ConfigFileUsed())

	// SafeWriteConfig
	if err := viper.SafeWriteConfig(); err != nil {
		fmt.Println("1保存配置失败", err.Error()) // 文件存在就会报错
	}

	// WriteConfigAs
	if err := viper.WriteConfigAs("./config/p.ini"); err != nil {
		fmt.Println("2保存配置失败", err.Error()) // 文件存在就会报错
	}

}

5. 开启自动重新加载配置文件

在读取完所有配置文件之后使用viper.WatchConfig()即可

 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
package main

import (
	"fmt"
	"time"

	"github.com/fsnotify/fsnotify"
	"github.com/spf13/viper"
)

func main() {
	/*
		1. 设置配置文件目录
	*/
	viper.AddConfigPath("./config")

	/*
		2. 读取第一个配置文件app.yaml
		配置内容为
		default:
		  filename: app.yaml
		  type: yaml
	*/
	viper.SetConfigName("app")
	viper.SetConfigType("yaml")
	if err := viper.ReadInConfig(); err != nil {
		fmt.Print(err.Error())
	}

    /*
        3. 文件监听的回调
	*/
	viper.OnConfigChange(func(e fsnotify.Event) {
		fmt.Println("配置文件发生变化:", e.Name)
		fmt.Println("操作类型:", e.Op.String())
	})

    // 启用文件监听
	viper.WatchConfig()

	for {
		fmt.Printf("当前所有配置:%+v \n", viper.AllSettings())
		time.Sleep(1 * time.Second)
	}

}

6. 参数别名

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

func main() {
	/*
		1. 设置配置文件目录
	*/
	viper.AddConfigPath("./config")

	/*
		2. 读取第一个配置文件app.yaml
		配置内容为
		default:
		  filename: app.yaml
		  type: yaml
	*/
	viper.SetConfigName("app")
	viper.SetConfigType("yaml")
	if err := viper.ReadInConfig(); err != nil {
		fmt.Print(err.Error())
	}
	fmt.Printf("读取app.yaml: %+v \n", viper.AllSettings())
	
	// 设置别名filename == default.filename
	viper.RegisterAlias("filename", "default.filename")
    
	fmt.Println("default.filename: ", viper.GetString("default.filename"))
	fmt.Println("filename: ", viper.GetString("filename"))
	fmt.Printf("读取app.yaml: %+v \n", viper.AllSettings()) // 添加别名后会多出别名的配置
}

7. 使用环境变量

使用环境变量有如下方式,如果多个地方设置了相同的参数,需要注意优先级(请看第一部分的简介)

  • SetEnvPrefix 设置环境变量前缀, XXX,viper会自动添加_
  • BindEnv 手动绑定一些环境变量作为参数
  • AutomaticEnv自动查找模式,在Get()时viper会根据查找的参数,去环境变量里找对应的值。比如Get("name"),就找NAME,有设置前缀的话就自动加前缀
  • SetEnvKeyReplacer如果使用Get("aa-bb")获取参数时,viper会从环境变量中查找AA-BB,但是变量名不能有-, 肯定会找不到,所以需要替换AA-BBAA_BB,使用方法请查看下面的示例
  • viper.AllowEmptyEnv(true)默认情况下,空环境变量被认为是未设置的,并根据优先级顺序查找config中的内容。使用AllowEmptyEnv方法将空的环境变量视为set。

示例

 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
package main

import (
	"fmt"
	"os"
	"strings"

	"github.com/spf13/viper"
)

func main() {
	/*
		使用BindEnv获取环境变量的值
	*/
	fmt.Println("使用BindEnv获取环境变量的值=========================================================")
	// 添加环境变量
	os.Setenv("MYAPP_NAME", "SoulChild")
	os.Setenv("NAME2", "随笔记")

	// 设置环境变量的前缀为 MYAPP_
	viper.SetEnvPrefix("MYAPP")

	// 将name和MYAPP_NAME绑定, 自动识别的环境变量名, 会带上前缀
	viper.BindEnv("name")
	// 将name2和NAME2绑定, 绑定指定的环境变量名, 不会带前缀
	viper.BindEnv("name2", "NAME2")

	// 通过name和name2获取参数值
	fmt.Println(viper.Get("name"))
	fmt.Println(viper.Get("name2"))

	os.Setenv("MYAPP_NAME", "newname") // 如果修改了环境变量, 获取到的值也会发生变化
	fmt.Println(viper.Get("name"))
	fmt.Println(viper.Get("name2"))

	/*
		使用AutomaticEnv自动绑定变量
	*/
	fmt.Println("使用AutomaticEnv=========================================================")
	// 添加环境变量
	os.Setenv("MYAPP_NAME2", "随笔记")

	// 设置环境变量的前缀为 MYAPP_
	viper.SetEnvPrefix("MYAPP")

	// 开启自动获取, 也就是Get("xx")的时候,自己去找对应的环境变量
	viper.AutomaticEnv()

	fmt.Println(viper.Get("name2")) // 从环境变量查找MYAPP_NAME2, 如果没有设置前缀就找NAME2

	/*
		SetEnvKeyReplacer 替换参数中的字符
	*/
	fmt.Println("SetEnvKeyReplacer=========================================================")
	// 设置环境变量
	os.Setenv("MYAPP_REFRESH_INTERVAL", "30s")

	// 创建替换器,将 `-` 替换为 `_`
	replacer := strings.NewReplacer("-", "_")
	// 设置替换器到viper实例中, viper内部会调用替换器的Replace()函数实现替换
	viper.SetEnvKeyReplacer(replacer)

	// 后续get带`-`的参数,都会替换为`_`
	fmt.Println(viper.Get("refresh-interval"))
    
    // 也可以自己实现viper.StringReplacer接口实现自定义替换。这个一般应该用不到,上面的方式足够用了
	// viper.NewWithOptions(viper.EnvKeyReplacer(strings.NewReplacer("-", "_"))) // 在实例化的时候操作

}

8. 使用命令行选项

 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/pflag"
	"github.com/spf13/viper"
)

func main() {
	pflag.IntP("port", "p", 80, `服务端口号`)
	pflag.StringP("LogLevel", "l", "INFO", `日志级别`)

	pflag.Parse()
	// 绑定命令行选项
	viper.BindPFlags(pflag.CommandLine)

	fmt.Println(viper.GetInt("port"))
	fmt.Println(viper.GetString("LogLevel"))
}

9. 远程配置中心

https://pkg.go.dev/github.com/spf13/viper#readme-remote-key-value-store-support

viper支持"etcd", “etcd3”, “consul”, “firestore"作为远程配置中心,

10. 将配置反序列化到结构体

 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
package main

import (
	"fmt"

	"github.com/spf13/pflag"
	"github.com/spf13/viper"
)

type Config struct {
	Port     int
	LogLevel string
}

func main() {
	pflag.IntP("port", "p", 80, `服务端口号`)
	pflag.StringP("LogLevel", "l", "INFO", `日志级别`)

	pflag.Parse()
	// 绑定命令行选项
	viper.BindPFlags(pflag.CommandLine)

	var c Config
	// 将viper读取到的配置都存入 c 中
	if err := viper.Unmarshal(&c); err != nil {
		fmt.Print(err.Error())
	}

	fmt.Println(c.Port, c.LogLevel)
}

viper.Unmarshal 如果viper中的配置在结构体中没有, 不会影响反序列化

viper.UnmarshalExact 如果viper中的配置在结构体中没有, 会panic

viper.UnmarshalKey 将某个配置,反序列化到结构体或者变量中. eg: viper.UnmarshalKey("port", &c.port)

11. 读取相关

通过GetType可以获取不同类型的参数,比如GetIntGetStringGetIntSlice.

具体文档在https://pkg.go.dev/github.com/spf13/viper#readme-getting-values-from-viper

通过IsSet判断参数是否存在,不要使用是否为空来判断

请我喝杯水
SoulChild 微信号 微信号
SoulChild 微信打赏 微信打赏
0%