golang-viper详解


原文链接: golang-viper详解

cobra 命令生成工具
viper 提供了配置文件的解析工作
pflag 提供命令行的解析工作

cfg := &ServerConfig{
RedisURL: viper.GetString("redis_port_6379_tcp") | viper.GetString("database_url"),
}

viper 是什么?

viper 为 Go 应用程序提供了一个完整的配置解决方案。它被设计用来和应用程序一起工作,能够处理所有类型的配置,它支持:

设置默认值
从 JSON、TOML、YAML、HCL 和 Java 特性配置文件中读取配置
可一直观察配置文件的改变并且重新读取配置文件
从环境变量中读取配置
从远程配置系统(etcd、Consul)中读取配置,并且一直观察配置的改变
从命令行的 flags 中读取配置
从 buffer 中读取配置
通过Set()明确指定的值
  • setting defaults
  • reading from JSON, TOML, YAML, HCL, and Java properties config files
  • live watching and re-reading of config files (optional)
  • reading from environment variables
  • reading from remote config systems (etcd or Consul), and watching changes
  • reading from command line flags
  • reading from buffer
  • setting explicit values 明确指定的

viper 可以被认为是一个你的应用程序所需要的所有配置的注册中心。

如何获取配置参数 Getting Values From Viper

In Viper, there are a few ways to get a value depending on the value’s type.
The following functions and methods exist:

  • Get(key string) : interface{}
  • GetBool(key string) : bool
  • GetFloat64(key string) : float64
  • GetInt(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{}

为何使用viper?

viper 使用以下优先级,越往下优先级越高

default                // 6. 默认参数
key/value store        // 5. 通过etcd、consul加载的参数
config                 // 4. 通过配置文件加载的参数
env                    // 3. 通过环境变量加载的参数
flag                   // 2. 通过命令行指定的参数
explicit call to Set() // 1. 通过Set()明确指定的参数

支持的配置文件格式

目前支持的解析类型为5种 "json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl"
但是不管使用何种解析格式,一个 Viper 的实例只支持一种配置文件格式.
ConfigType 有两种方式获取:

  1. 强制指定解析类型
    viper.SetConfigType("yaml")
  2. 根据所使用的文件扩展名进行推断
    config.yaml

  3. 查看正在使用的配置文件
    viper.ConfigFileUsed()

1. 使用别名(Registering and Using Aliases)

Aliases允许一个值多个键引用,例如

viper.RegisterAlias("Loud", "Verbose")
viper.Set("loud", "haha")                    //viper 不区分大小写
fmt.Println("Loud ", viper.Get("Loud"))      //输出:Loud haha
fmt.Println("verbose ", viper.Get("verbose"))//输出:verbose haha

2. 强制指定参数value (Setting Overrides)

viper.Set("Verbose", true)
viper.Set("LogFile", LogFile)

3. 使用命令行参数 flags

Viper 可以绑定到 flags,Viper 支持 Pflags (也在 cobra 库中使用)。

像 BindEnv,当绑定函数被调用的时候,值是没有被设置的,直到被访问的时候。这意味着你可以想多早就多早绑定,甚至可以在 init 函数内。

	var (
		configFile string
	)

	func init() {
		flag.StringVar(&configFile, "c", "", "config file")
		flag.Parse()
		// // using standard library "flag" package
		// flag.Int("port", 8081, "Port to listen on")
		// flag.String("blacklist", "blacklist.conf", "Location of blacklist file")

		// pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
		// pflag.Parse()
		// viper.BindPFlags(pflag.CommandLine)

		viper.AddConfigPath(".") 
		viper.AddConfigPath("$HOME/.config/") 
		// viper.SetConfigFile(configFile)
		viper.ReadInConfig()

		// viper.SetEnvPrefix("")                    //注意 spf 会被自动转换为 SPF
		viper.AutomaticEnv()
		viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
		if viper.GetString("mysql.host") == "" {
			panic("config error")
		}
	}

	func main() {
		fmt.Printf("previous mysql.host = %s\n", viper.GetString("mysql.host"))
		os.Setenv("MYSQL_HOST", "192.168.1.1")
		fmt.Printf("after mysql.host = %s\n", viper.GetString("mysql.host"))

		viper.WatchConfig()
		viper.OnConfigChange(func(e fsnotify.Event) {
			fmt.Printf("Config file changed: %v\n", e)
			fmt.Printf("reload mysql.host = %s\n", viper.GetString("mysql.host"))
		})

		go func() {
			for {
				time.Sleep(10 * time.Second)
				fmt.Printf("timer load mysql.host = %s\n", viper.GetString("mysql.host"))
				fmt.Printf("os load mysql.host = %s\n", os.Getenv("MYSQL_HOST"))
			}
		}()

		// shell command "export MYSQL_HOST=127.0.0.3" doesn't work

		time.Sleep(86400 * time.Second)
	}

4. 使用环境变量

viper 使用以下四种方法与环境变量打交道:

  • AutomaticEnv()
  • BindEnv(string...) : error
  • SetEnvPrefix(string)
  • SetEnvKeyReplacer(string...) *strings.Replacer

需要认识到的是,viper 与环境变量打交道时,是大小写敏感的。

viper 提供了一种机制来保证环境变量是唯一的,通过使用SetEnvPrefix(string)告诉 viper 读取环境变量的时候自动加上前缀,
格式形为:prefix + '_' + env;AutomaticEnv 和 BindEnv(string...) 都会使用该前缀。

BindEnv 函数需要一个或两个参数,第一个参数为 key,第二个参数为环境变量的名称,当只有一个参数的时候,viper 会自动去环境变量中匹配第一个 key 参数(注意,此时 key 会被 viper 实例自动转换为大写);当有两个参数的时候,viper 会将该 key 与指定的环境变量名称绑定。注意:如果显示指定了两个参数,则 BindEnv 不会自动加上前缀。

需要认识到的是,每一次 viper 实例访问环境变量的时候,都会重新读取,BindEnv 并不会将 key 与值相固定

AutomaticEnv 与 SetEnvPrefix 结合的时候相当强大,AutomaticEnv 被调用后,每一次调用viper.Get,viper 将会检查环境变量。viper 实例会自动将前缀加在 key (自动被转换为大写字母)上,如果 SetEnvPrefix 被调用的话。

SetEnvKeyReplacer 允许你使用 strings.Replacer 对象在某种程度上重写环境变量,比如你想在 viper.Get 调用中使用 - 或者别的间隔符,而在环境变量中使用 _ 间隔符的时候,SetEnvKeyReplacer 是很有用的。

viper.Automatic()
viper.SetEnvPrefix("spf")                    //注意 spf 会被自动转换为 SPF
os.Setenv("SPF_HASH", "12345")
os.Setenv("HASH", "54321")
fmt.Println("SPF_HASH ", viper.Get("hash"))  //输出:12345

//需将viper.Automatic注释掉,否则会有冲突,当有冲突的时候以viper.Automatic为准
viper.BindEnv("hash", "HASH")                
fmt.Println(viper.Get("hash"))             //输出:54321
viper.BindEnv("hash")
fmt.Println(viper.Get("hash"))             //输出:12345

5. 读取配置文件

当前,一个 Viper 的实例只支持一种配置文件格式.

加载配置文件的方法只有一个 ReadInConfig() ,在配置文件设置之后调用.
err := viper.ReadInConfig() //读取配置文件

但是配置文件的加载有两种方式:
通过源码我们可以发现,如果通过 SetConfigFile(configFile) 指定config文件之后,Viper将不在对目录搜索,而是直接将指定的配置文件返回

  1. 直接设置配置文件为一个文件(如果该项设置,以此为准,不在继续搜索)
    viper.SetConfigFile(configFile)

  2. 设置配置文件名称及查找路径
    Viper 并没有默认配置文件的搜索路径,所以应用程序需要告诉 Viper 实例配置文件的路径,这里有一个如何使用 Viper 搜索并读取配置文件的例子:
    v.configName = "config" // viper在初始化的时候就已经设置了默认的搜索文件名为config

viper.SetConfigName("config")  //配置文件的名称(不需要扩展名)
viper.AddConfigPath(".")       //将当前目录加入到 viper 的搜索路径中,优先搜索先添加的目录.
viper.AddConfigPath("$HOME/somedir")  //可多次调用AddConfigPath函数,注意:若多个搜索路径中均包含指定的配置文件,默认读取的是一个添加的搜索路径里的配置文件

// viper.SetConfigFile(configFile) //强制使用指定的配置文件,不在进行搜索
err := viper.ReadInConfig()    //搜索并且读取配置文件
if err != nil {
    panic(fmt.Errorf("Fatal error config file : %s \n", err))
}
fmt.Println(viper.Get("name")) //假设配置文件中包含name属性,那么 Get 可以得到该属性对应的值

6. 自定义配置文件源

只要实现了 io.Reader Reading Config from io.Reader

ReadInConfig() 只支持fs的配置文件读取,但是ReadConfig(in io.Reader) 就强大多了,只要实现io.Reader接口的都可以传递进来

但是由于使用了自定义的配置源,所以自动推断方式就可能出错,
比如 指定的配置文件为json格式 自定义的却是yaml格式,那么解析就会出错.
所以当使用 viper.ReadConfig(io.Reader) 时,必须设置解析文件的格式.

viper 预定义了很多配置源,例如文件、环境变量、flags 和远程 key/value 存储,
但是你不会被局限于此,你可以定义自己的配置文件源,并将其送给 viper, 例如

var yaml_ex = []byte(`
name : kk
age : 23
wife :
    name : ss           //注意:yaml文件只支持前面是空格,不支持tab键,所以确保属性的前面是空格而不是tab键
    age : 23
subject :
    - math
    - computer
`)
viper.SetConfigType("yaml")  // 1. 先设置要解析的文件类型
viper.ReadConfig(bytes.NewBuffer(yaml_ex)) // 2. 对文件类型进行解析
fmt.Println("name ", viper.Get("name"))

7. 通过etcd、consul加载的参数 Remote Key/Value Store Support

To enable remote support in Viper, do a blank import
import _ "github.com/spf13/viper/remote"

//1. http方式 Unencrypted 
viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001","/config/hugo.json")
viper.SetConfigType("json") // because there is no file extension in a stream of bytes, supported extensions are "json", "toml", "yaml", "yml", "properties", "props", "prop"
err := viper.ReadRemoteConfig()

//2. https方式 Encrypted
viper.AddSecureRemoteProvider("etcd","http://127.0.0.1:4001","/config/hugo.json","/etc/secrets/mykeyring.gpg")
viper.SetConfigType("json") // because there is no file extension in a stream of bytes,  supported extensions are "json", "toml", "yaml", "yml", "properties", "props", "prop"
err := viper.ReadRemoteConfig()

// unmarshal config
viper.Unmarshal(&runtime_conf)

// 2. consul 方式
var runtimeViper = viper.New()

	err := runtimeViper.AddRemoteProvider("consul", "localhost:8500", "dev/app/etc/conf")
	if err != nil {
		fmt.Println(err)
	}
	runtimeViper.SetConfigType("json")

	// err := runtimeViper.Unmarshal(&runtime_conf)

	go func() {
		for {
			time.Sleep(time.Second * 5)
			err := runtimeViper.WatchRemoteConfig()
			if err != nil {
				fmt.Println(err)
				continue
			}

			// runtimeViper.Unmarshal(&runtime_conf)
			port := runtimeViper.GetInt("http.port")
			address := runtimeViper.GetString("http.address")
			fmt.Println(address, port)
		}
	}

8. 使用参数的默认值

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

9. 监控重新读取配置文件(Watching and re-reading config files)

viper 支持你的应用程序运行时观察到并读取配置文件的改变,重启服务才能让你的配置文件生效已经成为过去,viper 可以很轻松的帮助你读取到更新过后的文件,告诉 viper 实例观察配置文件(WatchConfig),你还可以传进去一个函数来告诉 viper 实例当配置文件改变时应该做什么,例如

viper.WatchConfig()
viper.OnConfigChange(fun (e fsnotify.Event) {
    fmt.Println("config files changed, afer change, the name become : ", viper.Get("name"))
})
`