Golang 配置信息处理库Viper(翻译)

项目地址:https://github.com/spf13/viper

有很多Go语言项目用到了Viper框架,比如:

安装

go get -u github.com/spf13/viper

什么是Viper?

Viper是一个方便Go语言应用程序处理配置信息的库。它可以处理多种格式的配置。它支持的特性:

  • 设置默认值
  • 从JSON、TOML、YAML、HCL和Java properties文件中读取配置数据
  • 可以监视配置文件的变动、重新读取配置文件
  • 从环境变量中读取配置数据
  • 从远端配置系统中读取数据,并监视它们(比如etcd、Consul)
  • 从命令参数中读物配置
  • 从buffer中读取
  • 调用函数设置配置信息

为什么要使用Viper?

在构建现代应用程序时,您不必担心配置文件格式; 你可以专注于构建出色的软件。
Viper 可以做如下工作:

  • 加载并解析JSON、TOML、YAML、HCL 或 Java properties 格式的配置文件
  • 可以为各种配置项设置默认值
  • 可以在命令行中指定配置项来覆盖配置值
  • 提供了别名系统,可以不破坏现有代码来实现参数重命名
  • 可以很容易地分辨出用户提供的命令行参数或配置文件与默认相同的区别

Viper读取配置信息的优先级顺序,从高到低,如下:

  • 显式调用Set函数
  • 命令行参数
  • 环境变量
  • 配置文件
  • key/value 存储系统
  • 默认值

Viper 的配置项的key不区分大小写。

设置值

设置默认值

默认值不是必须的,如果配置文件、环境变量、远程配置系统、命令行参数、Set函数都没有指定时,默认值将起作用。
例子:

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

读取配置文件

Viper支持JSON、TOML、YAML、HCL和Java properties文件。
Viper可以搜索多个路径,但目前单个Viper实例仅支持单个配置文件。
Viper默认不搜索任何路径。
以下是如何使用Viper搜索和读取配置文件的示例。
路径不是必需的,但最好至少应提供一个路径,以便找到一个配置文件。

viper.SetConfigName("config") //  设置配置文件名 (不带后缀)
viper.AddConfigPath("/etc/appname/")   // 第一个搜索路径
viper.AddConfigPath("$HOME/.appname")  // 可以多次调用添加路径
viper.AddConfigPath(".")               // 比如添加当前目录
err := viper.ReadInConfig() // 搜索路径,并读取配置数据
if err != nil {
    panic(fmt.Errorf("Fatal error config file: %s \n", err))
}

监视配置文件,重新读取配置数据

Viper支持让您的应用程序在运行时拥有读取配置文件的能力。
需要重新启动服务器以使配置生效的日子已经一去不复返了,由viper驱动的应用程序可以在运行时读取已更新的配置文件,并且不会错过任何节拍。
只需要调用viper实例的WatchConfig函数,你也可以指定一个回调函数来获得变动的通知。

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

从 io.Reader 中读取配置

Viper预先定义了许多配置源,例如文件、环境变量、命令行参数和远程K / V存储系统,但您并未受其约束。
您也可以实现自己的配置源,并提供给viper。

viper.SetConfigType("yaml") // or viper.SetConfigType("YAML")

// any approach to require this configuration into your program.
var yamlExample = []byte(`
Hacker: true
name: steve
hobbies:
- skateboarding
- snowboarding
- go
clothing:
  jacket: leather
  trousers: denim
age: 35
eyes : brown
beard: true
`)

viper.ReadConfig(bytes.NewBuffer(yamlExample))

viper.Get("name") // 返回 "steve"

Set 调用

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

注册并使用别名

别名可以实现多个key引用单个值。

viper.RegisterAlias("loud", "Verbose")

viper.Set("verbose", true) 
viper.Set("loud", true)   // 这两句设置的都是同一个值

viper.GetBool("loud") // true
viper.GetBool("verbose") // true

从环境变量中读取

Viper 完全支持环境变量,这是的应用程序可以开箱即用。
有四个和环境变量有关的方法:

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

注意,环境变量时区分大小写的。

Viper提供了一种机制来确保Env变量是唯一的。通过SetEnvPrefix,在从环境变量读取时会添加设置的前缀。BindEnv和AutomaticEnv都会使用到这个前缀。

BindEnv需要一个或两个参数。第一个参数是键名,第二个参数是环境变量的名称。环境变量的名称区分大小写。如果未提供ENV变量名称,则Viper会自动假定该键名称与ENV变量名称匹配,并且ENV变量为全部大写。当您显式提供ENV变量名称时,它不会自动添加前缀。

使用ENV变量时要注意,当关联后,每次访问时都会读取该ENV值。Viper在BindEnv调用时不读取ENV值。

AutomaticEnv与SetEnvPrefix结合将会特别有用。当AutomaticEnv被调用时,任何viper.Get请求都会去获取环境变量。环境变量名为SetEnvPrefix设置的前缀,加上对应名称的大写。

SetEnvKeyReplacer允许你使用一个strings.Replacer对象来将配置名重写为Env名。如果你想在Get()中使用包含-的配置名 ,但希望对应的环境变量名包含_分隔符,就可以使用该方法。使用它的一个例子可以在项目中viper_test.go文件里找到。
例子:

SetEnvPrefix("spf") // 将会自动转为大写
BindEnv("id")
os.Setenv("SPF_ID", "13") // 通常通过系统环境变量来设置
id := Get("id") // 13

绑定命令行参数

Viper支持绑定pflags参数。
和BindEnv一样,当绑定方法被调用时,该值没有被获取,而是在被访问时获取。这意味着应该尽早进行绑定,甚至是在init()函数中绑定。

利用BindPFlag()方法可以绑定单个flag。
例子:

serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))

你也可以绑定已存在的pflag集合 (pflag.FlagSet):

pflag.Int("flagname", 1234, "help message for flagname")

pflag.Parse()
viper.BindPFlags(pflag.CommandLine)

i := viper.GetInt("flagname") // 通过viper从pflag中获取值

使用pflag并不影响其他库使用标准库中的flag。通过导入,pflag可以接管通过标准库的flag定义的参数。这是通过调用pflag包中的AddGoFlagSet()方法实现的。
例子:

package main

import (
    "flag"
    "github.com/spf13/pflag"
)

func main() {

    // using standard library "flag" package
    flag.Int("flagname", 1234, "help message for flagname")

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

    i := viper.GetInt("flagname") // retrieve value from viper

    ...
}

Flag 接口

如果你不想使用pflag,Viper 提供了两个接口来实现绑定其他的flag系统。
使用 FlagValue 接口代表单个flag。下面是实现了该接口的简单的例子:

type myFlag struct {}
func (f myFlag) HasChanged() bool { return false }
func (f myFlag) Name() string { return "my-flag-name" }
func (f myFlag) ValueString() string { return "my-flag-value" }
func (f myFlag) ValueType() string { return "string" }

一旦你实现了该接口,就可以绑定它:

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

使用 FlagValueSet 接口代表一组flag。下面是实现了该接口的简单的例子:

type myFlagSet struct {
    flags []myFlag
}

func (f myFlagSet) VisitAll(fn func(FlagValue)) {
    for _, flag := range flags {
        fn(flag)
    }
}

一旦你实现了该接口,就可以绑定它:

fSet := myFlagSet{
    flags: []myFlag{myFlag{}, myFlag{}},
}
viper.BindFlagValues("my-flags", fSet)

支持远程 Key/Value 存储

启用该功能,需要导入viper/remot包:

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

Viper 可以从例如etcd、Consul的远程Key/Value存储系统的一个路径上,读取一个配置字符串(JSON, TOML, YAML或HCL格式)。
这些值优先于默认值,但会被从磁盘文件、命令行flag、环境变量的配置所覆盖。

Viper 使用 crypt 来从 K/V 存储系统里读取配置,这意味着你可以加密储存你的配置信息,并且可以自动解密配置信息。加密是可选的。

您可以将远程配置与本地配置结合使用,也可以独立使用。

crypt 有一个命令行工具可以帮助你存储配置信息到K/V存储系统,crypt默认使用 http://127.0.0.1:4001 上的etcd。

$ go get github.com/xordataexchange/crypt/bin/crypt
$ crypt set -plaintext /config/hugo.json /Users/hugo/settings/config.json

确认你的值被设置:

$ crypt get -plaintext /config/hugo.json

有关crypt如何设置加密值或如何使用Consul的示例,请参阅文档。

远程 Key/Value 存储例子 - 未加密的

viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001","/config/hugo.json")
viper.SetConfigType("json") // 因为不知道格式,所以需要指定,支持的格式有"json"、"toml"、"yaml"、"yml"、"properties"、"props"、"prop"
err := viper.ReadRemoteConfig()

远程 Key/Value 存储例子 - 加密的

viper.AddSecureRemoteProvider("etcd","http://127.0.0.1:4001","/config/hugo.json","/etc/secrets/mykeyring.gpg")
viper.SetConfigType("json") // 因为不知道格式,所以需要指定,支持的格式有"json"、"toml"、"yaml"、"yml"、"properties"、"props"、"prop"
err := viper.ReadRemoteConfig()

监视etcd的变化 - 未加密的

// 您可以创建一个新的viper实例
var runtime_viper = viper.New()

runtime_viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001", "/config/hugo.yml")
runtime_viper.SetConfigType("yaml") // 因为不知道格式,所以需要指定,支持的格式有"json"、"toml"、"yaml"、"yml"、"properties"、"props"、"prop"

// 从远程读取配置
err := runtime_viper.ReadRemoteConfig()

// 解析配置到runtime_conf中
runtime_viper.Unmarshal(&runtime_conf)

// 通过一个goroutine远程的配置变化
go func(){
    for {
        time.Sleep(time.Second * 5) // delay after each request

        // currently, only tested with etcd support
        err := runtime_viper.WatchRemoteConfig()
        if err != nil {
            log.Errorf("unable to read remote config: %v", err)
            continue
        }

            // 解析新的配置到一个结构体变量中,你也可以使用channel实现一个信号通知的方式
        runtime_viper.Unmarshal(&runtime_conf)
    }
}()

获取值

在Viper中,有一些根据值的类型获取值的方法。存在一下方法:

  • 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

如果Get函数未找到值,则返回对应类型的一个零值。可以通过 IsSet() 方法来检测一个健是否存在。
例子:

viper.GetString("logfile") // Setting & Getting 不区分大小写
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") // (returns "127.0.0.1")

这遵守前面确立的优先规则; 会搜索路径中所有配置,直到找到为止。
例如,上面的文件,datastore.metric.host和 datastore.metric.port都已经定义(并且可能被覆盖)。如果另外 datastore.metric.protocol的默认值,Viper也会找到它。

但是,如果datastore.metric值被覆盖(通过标志,环境变量,Set方法,…),则所有datastore.metric的子键将会未定义,它们被优先级更高的配置值所“遮蔽”。

最后,如果存在相匹配的嵌套键,则其值将被返回。例如:

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

提取子树配置

可以从viper中提取子树。例如, viper配置为:

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

执行后:

subv := viper.Sub("app.cache1")

subv 就代表:

max-items: 100
item-size: 64

假如我们有如下函数:

func NewCache(cfg *Viper) *Cache {...}

它的功能是根据配置信息创建缓存缓存。现在很容易分别创建这两个缓存:

cfg1 := viper.Sub("app.cache1")
cache1 := NewCache(cfg1)

cfg2 := viper.Sub("app.cache2")
cache2 := NewCache(cfg2)

解析配置

您还可以选择将所有或特定值解析到struct、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 := Unmarshal(&C)
if err != nil {
    t.Fatalf("unable to decode into struct, %v", err)
}

使用单个viper还是多个viper

Viper随时准备使用开箱即用。没有任何配置或初始化也可以使用Viper。由于大多数应用程序都希望使用单个存储中心进行配置,因此viper包提供了此功能。它类似于一个单例模式。

在上面的所有示例中,他们都演示了如何使用viper的单例风格的方式。

使用多个viper实例

您还可以创建多不同的viper实例以供您的应用程序使用。每实例都有自己独立的设置和配置值。每个实例可以从不同的配置文件,K/V存储系统等读取。viper包支持的所有函数也都有对应的viper实例方法。
例子:

x := viper.New()
y := viper.New()

x.SetDefault("ContentDir", "content")
y.SetDefault("ContentDir", "foobar")

//...

当使用多个viper实例时,用户需要自己管理每个实例。

示例代码

package main

import (
    "flag"
    "fmt"
    "github.com/fsnotify/fsnotify"
    "github.com/spf13/pflag"
    "github.com/spf13/viper"
    _ "github.com/spf13/viper/remote"
    "log"
    "os"
    "strings"
    "time"
)

func main() {
    aboutRemote()
    aboutFlags()
    aboutDefault()
    aboutConfigFile()
    aboutWatchingAndReload()
    aboutAliases()
    aboutEnvVars()
}

func aboutRemote() {
    log.Println("\n--------------------------------------about remote configure")

    //etcd v3版本不支持
    //viper.AddRemoteProvider("etcd", "http://localhost:2379", "viperTestConfig")
    //viper.SetConfigType("json") // because there is no file extension in a stream of bytes, supported extensions are "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv"

    //if err := viper.ReadRemoteConfig();err!=nil{
    //    log.Fatalln(err)
    //}
    //log.Println(viper.Get("p3"))

    //Consul
    viper.AddRemoteProvider("consul", "localhost:8500", "MY_CONSUL_KEY")
    viper.SetConfigType("json") // Need to explicitly set this to json

    if err := viper.ReadRemoteConfig();err!=nil{
        log.Fatalln(err)
    }

    fmt.Println(viper.Get("port")) // 8080
    fmt.Println(viper.Get("hostname")) // myhostname.com
}

func aboutFlags() {
    log.Println("\n--------------------------------------about flags")

    //使用标准库flag
    flag.Int("port", 1234, "help message for port")
    pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
    //使用pflag
    pflag.String("host", "127.0.0.1", "help message for host")

    pflag.Parse()

    viper.BindPFlags(pflag.CommandLine)

    port := viper.GetInt("port")
    host := viper.GetString("host")

    log.Println("flag ->", host, port)
}

func aboutEnvVars() {
    log.Println("\n--------------------------------------about ENV vars")
    //viper对环境变量大小心敏感

    viper.SetEnvPrefix("VT")
    //如果BindEnv()第二个参数指定后,不需要prefix
    if err := viper.BindEnv("myGoPath", "GOPATH"); err != nil {
        log.Fatalln(err)
    }
    //如果BindEnv只有一个参数,默认会绑定: strings.ToUpper(Prefix + "_" + key)
    os.Setenv("VT_ROOT", "i am root")
    if err := viper.BindEnv("Root"); err != nil {
        log.Fatalln(err)
    }
    log.Println("ENV myGoPath->", viper.Get("myGoPath")) // true
    log.Println("ENV myGoRoot->", viper.Get("Root"))     // true

    //读取key的时候,尝试从环境变量中查找
    viper.AutomaticEnv()
    os.Setenv("VT_P1", "321")
    log.Println("ENV p1->", viper.Get("p1"))

    replacer := strings.NewReplacer("-", "_")
    viper.SetEnvKeyReplacer(replacer)
    os.Setenv("VT_RE_PLACER", "replacer...")
    log.Println("ENV replacer->", viper.Get("re-placer"))

    viper.AllowEmptyEnv(true)
}

func aboutAliases() {
    log.Println("\n--------------------------------------about aliases")

    //注册别名
    viper.RegisterAlias("loud", "Verbose")

    viper.Set("verbose", true) // same result as next line
    viper.Set("loud", true)    // same result as prior line

    log.Println("Get ->", viper.GetBool("loud"))    // true
    log.Println("Get ->", viper.GetBool("verbose")) // true
}

func aboutWatchingAndReload() {
    log.Println("\n--------------------------------------about watching & reload")

    viper.WatchConfig()
    viper.OnConfigChange(func(e fsnotify.Event) {
        log.Println("Config file changed:", e.Name)
        //re-reading
    })
    //简单等待一下
    time.Sleep(5 * time.Second)
}

func aboutConfigFile() {
    log.Println("\n--------------------------------------about config file")

    //设置配置文件的名字,不带拓展名(必须的),viper根据拓展名解析文件格式
    viper.SetConfigName("viperTestConfig")
    //添加配置文件的查找路径(可能存在的路径)
    //在前边的先发现,优先使用,忽略后边发现的
    viper.AddConfigPath(".")      //当前工作路径
    viper.AddConfigPath("./conf") //其他路径

    //可以直接指定文件路径,这时必须明确文件名,不然无法解析
    //不需要再添加查询路径
    //viper.SetConfigFile("./configureFile.json")
    //viper.SetConfigFile("./configureFile.yaml")

    //从配置文件读取内容
    err := viper.ReadInConfig()
    if err != nil {
        log.Fatalln("Fatal error config file:", err)
    }

    //更新配置文件
    viper.Set("now", time.Now().Unix())
    viper.Set("array", []int{1, 2, 3, 4, 5, 6, 7}) //array
    viper.Set("map", map[string]interface{}{
        "key_int":    1,
        "key_string": "a string",
        "key_array":  []int{3, 4, 5, 6, 7},
        "key_map":    map[int]string{1: "a", 2: "b"},
    }) //array
    viper.WriteConfig()

    log.Println("Get ->", viper.Get("p2"))
}

func aboutDefault() {
    log.Println("\n--------------------------------------about default")
    //设置默认值
    viper.SetDefault("p1", 0)

    //没有设置,获得nil
    log.Println("Get ->", viper.Get("p0"))
    //正常获取
    log.Println("Get ->", viper.Get("p1"))
    //会尝试类型转换
    log.Println("GetInt ->", viper.GetInt("p1"))
    log.Println("GetString ->", viper.GetString("p1"))
    log.Println("GetBool ->", viper.GetBool("p1"))
    log.Println("GetFloat64 ->", viper.GetFloat64("p1"))
    log.Println("GetDuration ->", viper.GetDuration("p1")) //整数转时间,单位为纳秒
    log.Println("GetTime ->", viper.GetTime("p1"))         //整数转时间,使用unixtime
    //无法转换,获得空值
    log.Println("GetIntSlice ->", viper.GetIntSlice("p1"))
}

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 irvin.em@live.com。

文章标题:Golang 配置信息处理库Viper(翻译)

文章字数:4.4k

本文作者:dino

发布时间:2019-12-02, 18:34:36

最后更新:2019-12-04, 23:29:32

原始链接:https://blog.walkbc.com/2019/12/02/viper/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

QQ交流群:273078549

目录