1. 程式人生 > >Golang的配置資訊處理框架Viper【專案地址:https://github.com/spf13/viper】

Golang的配置資訊處理框架Viper【專案地址:https://github.com/spf13/viper】

轉自:http://blog.51cto.com/13599072/2072753

Viper

專案地址:https://github.com/spf13/viper

本文翻譯自該專案裡README.md檔案中的內容

有很多Go語言專案用到了Viper框架,比如:

  • Hugo
  • EMC RexRay
  • Imgur’s Incus
  • Nanobox/Nanopack
  • Docker Notary
  • BloomApi
  • doctl
  • Clairctl

什麼是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例項時,使用者需要自己管理每個例項。

問答

問:為什麼不使用INI檔案?

答:Ini檔案非常糟糕。沒有標準格式,而且很難驗證。Viper設計使用JSON、TOML或YAML檔案。如果有人真的想要新增此功能,專案的作者很樂意合併它。指定應用程式允許的格式很容易。

問:為什麼叫“Viper”?

答:Viper是Cobra專案的同伴。雖然兩者都可以完全獨立運作,但它們一起可以為您的應用程式做非常多的基礎工作。

問:為什麼叫“Cobra”?
答:還能為commander取一個更好的名字嗎?