什么是Viper

Viper是go应用程序的完全配置解决方案。可以处理所有类型和格式的配置需求。支持:

  • 设置默认值
  • 从JSON,TOML,YAML,HCL,envfile和Java属性配置文件中读取。
  • 监控和重新读取配置文件(可选的)。
  • 从远程配置系统(etcd或者Consul)读取,并监控变化。
  • 从命令行选项读取。
  • 从buffer中读取。
  • 设置明确的值。

可以将Viper视为满足所有应用程序配置需求的注册表。

Viper的优点

Viper可以实现如下功能:

  1. 找到,加载并解析JSON,TOML,YAML,HCL,INI,envfile和Java属性格式的配置文件。
  2. 提供为不同的配置选项设置默认值的机制。
  3. 提供一种机制来让命令行标志指定的选项的值覆盖对应的值。
  4. 提供了一个别名系统,能够在不破坏现有代码的情况下重命名参数。
  5. 用户提供的命令行或者配置文件与默认值相同时,能够容易识别出不同。

Viper中的优先级从高到低为:

  1. 显式调用Set
  2. flag。
  3. env。
  4. config。
  5. key/value store
  6. default。

Note

Viper的配置键是不区分大小写的,正在考虑是否设置为可选项。

将值存入Viper

建立默认值

一个好的配置系统需要支持默认值。在一个键没有在配置文件,环境变量,远程配置或者flag中设置的时候非常有用。 示例:

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

读取配置文件

Viper支持JSON,TOML,YAML,HCL,INI,envfile和Java属性文件。 Viper可以搜索多条路径,但是只有一个Viper实例只支持一个配置文件。Viper没有默认的配置搜索路径,需要应用程序设置。

下面有一个使用Viper搜索和读取配置文件的示例,没有一个特定的路径是必需的,但是在需要配置文件的地方至少应该提供一个路径。

// 配置文件名
viper.SetConfigName("config")
// 如果配置文件名中没有扩展名,需要指定扩展名
viper.SetConfigType("yaml")
// 设置搜索路径
viper.AddConfigPath("/etc/appname/")
viper.AddConfigPath("$HOME/.appname")
viper.AddConfigPath(".")
// 查找和读取配置文件
err := viper.ReadInConfig()
if err != nil {
	if _, ok := err.(viper.ConfigFileNotFoundError); ok {
	} else {
	}
}

Note

实际的配置文件可以没有扩展名,在代码中指明文件格式即可。

写配置文件

存储在运行过程中的修改。可以使用一系列命令:

  • WriteConfig:如果存在预定义的路径,将当前的Viper配置写入。如果没有预定义的路径,则错误。如果存在当前的配置文件,会将其覆盖。
  • SafeWriteConfig:将当前的viper配置写入预定义路径,不存在预定义路径则出错。如果当前配置文件存在,不会将其覆盖
  • WriteConfigAs:将当前的viper写入给定的文件路径。给定的文件如果存在则会覆盖。
  • SafeWriteConfigAs:将当前的viper配置写入给定的文件路径。给定文件存在不会覆盖。 就是说,以”Safe”开头的不会覆盖文件。文件不存在会创建。 示例:
viper.WriteConfig()
viper.SafeWriteConfig()
viper.WriteConfigAs("/path/to/my/.config")
viper.SafeWriteConfigAs("/path/to/my/.config") //已经存在会报错
viper.SafeWriteConfigAs("/path/to/my/.other_config")

监控与重读配置文件

viper支持在运行中实时读取一个配置文件的能力。 不必通过重启应用来让配置生效。 只需告诉viper实例watchConfig。您还可以为Viper提供一个函数,以便在每次发生更改时运行。

viper.OnConfigChange(func(e fsnotify.Event) {
	fmt.Println("Config file changed:", e.Name)
})
viper.WatchConfig()

从io.Reader中读取配置

Viper预先定义了许多配置源,例如文件,环境变量,标志和远程K/V存储,但不配置它们。您还可以实现自己所需的配置源并将其给到Viper。

viper.SetConfigType("yaml") // or viper.SetConfigType("YAML")
 
// 配置
var yamlExample = []byte(`
Hacker: true
name: steve
hobbies:
- skateboarding
- snowboarding
- go
clothing:
  jacket: leather
  trousers: denim
age: 35
eyes : brown
beard: true
`)
// 从buffer读取配置
viper.ReadConfig(bytes.NewBuffer(yamlExample))
 
viper.Get("name") // this would be "steve"

覆盖设置

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

注册和使用别名

别名可以让一个值被多个键引入。

viper.RegisterAlias("loud", "Verbose")
 
// 下面两条的作用是一样的
viper.Set("verbose", true)
viper.Set("loud", true)
 
viper.GetBool("loud")
viper.GetBool("verbose")

使用环境变量

Viper对环境变量有完善的支持,有以下五个方法:

  • AutomaticEnv()
  • BindEnv(string ...) : error
  • SetEnvPrefix(string)
  • SetEnvKeyReplace(string...) *strings.Replacer
  • AllowEmptyEnv(bool) 在处理ENV变量时,务必认识到Viper将ENV变量视为区分大小写的

环境变量都是独一无二的。使用SetEnvPrefix之后,Viper读取环境变量就会使用设置的前缀了,BindEnvAutomaticEnv也会使用设置的前缀。

BindEnv接受多个参数,第一个参数是键名,其余的是绑定到这个键的环境变量,按照指定的顺序优先。如果没有指定环境变量,那么会自动去prefix+_+key全大写的的环境变量。

AutomaticEnvSetEnvPrefix结合非常强大。调用它的时候,viper就会进行环境变量检查并发起viper.Get,会将环境变量的前缀与全大写的键名作为环境变量名。

示例:

// 设置前缀
viper.SetEnvPrefix("spf")
// 没有指定环境变量,将prefix_key全大写作为绑定的环境变量
viper.BindEnv("id")
 
os.Setenv("SPF_ID", "13")
 
id := viper.Get("id") // 13

使用Flags

Viper通过Pflags支持绑定选项。

就像BindEnv,值不是在调用该函数的时候绑定的,而是访问值的时候绑定的,意味着可以尽早绑定。

可以使用BindPFlag绑定单个flag。示例:

pflag.Int("port", 1138, "Port to run Application server on")
viper.BindPFlag("port", pflag.Lookup("port"))
t.Log(viper.Get("port"))

也可以绑定pflags集合:

pflag.Int("port", 1138, "Port to run Application server on")
pflag.Int("flagname", 1234, "help message for flagname")
pflag.Parse()
viper.BindPFlags(pflag.CommandLine)
t.Log(viper.Get("port"))
t.Log(viper.Get("flagname"))

使用pflag并不会排斥使用了标准库中flag库的其它包,pflag包可以通过导入标志来处理flag包的标志,通过调用pflag包提供的AddGoFlagSet即可完成。示例如下:

flag.Int("flagname", 1234, "help message for flagname")
 
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
 
pflag.Parse()
viper.BindPFlags(pflag.CommandLine)
t.Log(viper.GetInt("flagname"))

如果不想使用pflags,viper提供了两个Go接口来绑定其它flag系统,只要实现FlagValue接口,就可以调用

viper.BindFlagValue("my-flag-name", myFlag{})

进行绑定。

实现FlagValueSet接口,可以绑定FlagValue的集合。调用viper.BindFlagValues即可。

远程K/V存储支持

只需要空白导入viper/remote即可开启远程支持。

import _ "github.com/spf13/viper/remote"

viper将会从远程的键值存储例如etcd或者Consul的路径上读取配置字符串(JSON,TOML,YAML,HCL或者envfile)。

优先级要高于默认值,但比其它几种方式要低。

viper使用crypt来从KV存储中获取配置,意味着是可以进行自动加密和解密的。这是个可选项。

远程读取的示例为:

viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001","/config/hugo.json")
viper.SetConfigType("json")
err := viper.ReadRemoteConfig()

如果需要加密,只需要额外一个参数,指明加密文件。

viper.AddSecureRemoteProvider("etcd","http://127.0.0.1:4001","/config/hugo.json","/etc/secrets/mykeyring.gpg")
viper.SetConfigType("json") 
err := viper.ReadRemoteConfig()

从Viper中读取值

Viper提供如下方法从中读取值:

  • Get(key string) interface{}
  • GetBool(key string) bool
  • GetFloat64(key string) float64
  • GetInt(key string) int
  • GetIntSlice(key string) []int
  • GetString(key string) string
  • GetStringMap(key string) map[string]interface{}
  • GetStringSlice(key string) []string
  • GetTime(key string) time.Time
  • GetDuration(key string) time.Duration
  • IsSet(key string) bool
  • AllSettings() map[string]interface{}

Note

如果没有找到,Get函数会返回零值。IsSet()方法会检测是否存在给定的键

示例:

viper.GetString("logfile")
if viper.GetBool("verbose") {
	fmt.Println("verbose enabled")
}

访问嵌套的键

访问方法支持嵌套的键的访问,例如下面的JSON文件

{
    "host": {
        "address": "localhost",
        "port": 5799
    },
    "datastore": {
        "metric": {
            "host": "127.0.0.1",
            "port": 3099
        },
        "warehouse": {
            "host": "198.0.0.1",
            "port": 2112
        }
    }
}

viper通过.作为键的路径分隔符来访问嵌套的字段:

GetString("datastore.metric.host")

对于数组直接通过下标即可访问。

{
    "host": {
        "address": "localhost",
        "ports": [
            5799,
            6029
        ]
    },
    "datastore": {
        "metric": {
            "host": "127.0.0.1",
            "port": 3099
        },
        "warehouse": {
            "host": "198.0.0.1",
            "port": 2112
        }
    }
}
 
GetInt("host.ports.1") // returns 6029

但是如果存在一个键与分隔键路径匹配,就会用它替代:

{
    "datastore.metric.host": "0.0.0.0",
    "host": {
        "address": "localhost",
        "port": 5799
    },
    "datastore": {
        "metric": {
            "host": "127.0.0.1",
            "port": 3099
        },
        "warehouse": {
            "host": "198.0.0.1",
            "port": 2112
        }
    }
}
 
GetString("datastore.metric.host") // returns "0.0.0.0"

提取子树

在开发可重用模块时,提取配置的子集并将其传递给模块通常很有用。通过这种方式,模块可以使用不同的配置多次实例化。 例如,一个应用程序可以为了不同的目的使用不同的cache存储。

cache:
  cache1:
    max-items: 100
    item-size: 64
  cache2:
    max-items: 200
    item-size: 80

通过将配置传递给子模块可以实现与全局配置的分离

cache1Config := viper.Sub("cache.cache1")
if cache1Config == nil { // Sub returns nil if the key cannot be found
	panic("cache configuration not found")
}
 
cache1 := NewCache(cache1Config)

Unmarshaling

还可以选择将指定值或者全部值unmarshaling为结构体,map等等。 有两个函数可以实现这点:

  • Unmarshal(rawVal interface{}) error
  • UnmarshalKey(key string, rawVal interface{}) error

示例:

type config struct {
	Port int
	Name string
	PathMap string `mapstructure:"path_map"`
}
 
var C config
 
err := viper.Unmarshal(&C)
if err != nil {
	t.Fatalf("unable to decode into struct, %v", err)
}

如果想要解构键本身就包含点分隔符的,需要更改分隔符:

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",
		},
	},
})
 
type config struct {
	Chart struct{
		Values map[string]interface{}
	}
}
 
var C config
 
v.Unmarshal(&C)

Viper还支持将数据解构到嵌入的结构中

/*
Example config:
 
module:
    enabled: true
    token: 89h3f98hbwf987h3f98wenf89ehf
*/
type config struct {
	Module struct {
		Enabled bool
 
		moduleConfig `mapstructure:",squash"`
	}
}
 
// moduleConfig could be in a module specific package
type moduleConfig struct {
	Token string
}
 
var C config
 
err := viper.Unmarshal(&C)
if err != nil {
	t.Fatalf("unable to decode into struct, %v", err)
}

Viper在使用github.com/mitchellh/mapstructure,使用mapstructure作为解构值时的默认标签。

解码自定义格式

viper经常被请求支持更多格式的值和解码器,例如解析以点,逗号,分号等等作为分隔符的字符串为切片。 这已经在Viper中使用mapstructure解码hooks实现了。

编码到string

可能需要将viper的所有设置都编码到一个字符串而不是写入文件,可以使用你最喜欢的格式编码器来将AllSettings()返回的配置进行编码。

import (
	yaml "gopkg.in/yaml.v2"
	// ...
)
 
func yamlStringSettings() string {
	c := viper.AllSettings()
	bs, err := yaml.Marshal(c)
	if err != nil {
		log.Fatalf("unable to marshal config to YAML: %v", err)
	}
	return string(bs)
}

使用多个Viper

您还可以创建许多不同的viper用于您的应用程序。每个都有自己独特的配置和值集。每个可以读取不同的配置文件,键值存储,等等。viper包支持的所有函数都映射为为viper上的方法。 示例:

x := viper.New()
y := viper.New()
 
x.SetDefault("ContentDir", "content")
y.SetDefault("ContentDir", "foobar")
 
//...