Golang 的配置信息處理框架 Viper
項目地址:https://github.com/spf13/viper
本文翻譯自該項目裏 README.md 文件中的內容
有很多 Go 語言項目用到了 Viper 框架,比如:
- Hugo
- EMC RexRay
- Imgur’s Incus
- Nanobox/Nanopack
- Docker Notary
- BloomApi
- doctl
- Clairctl
Viper 是一個方便 Go 語言應用程序處理配置信息的庫。它可以處理多種格式的配置。它支持的特性:
- 設置默認值
- 從 JSON、TOML、YAML、HCL 和 Java properties 文件中讀取配置數據
- 可以監視配置文件的變動、重新讀取配置文件
- 從環境變量中讀取配置數據
- 從遠端配置系統中讀取數據,並監視它們(比如 etcd、Consul)
- 從命令參數中讀物配置
- 從 buffer 中讀取
- 調用函數設置配置信息
在構建現代應用程序時,您不必擔心配置文件格式; 你可以專注於構建出色的軟件。
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")
viper.GetBool("verbose")
從環境變量中讀取
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")
綁定命令行參數
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() {
flag.Int("flagname", 1234, "help message for flagname")
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
pflag.Parse()
viper.BindPFlags(pflag.CommandLine)
i := viper.GetInt("flagname")
...
}
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 的變化 - 未加密的
var runtime_viper = viper.New()
runtime_viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001", "/config/hugo.yml")
runtime_viper.SetConfigType("yaml")
err := runtime_viper.ReadRemoteConfig()
runtime_viper.Unmarshal(&runtime_conf)
go func(){
for {
time.Sleep(time.Second * 5)
err := runtime_viper.WatchRemoteConfig()
if err != nil {
log.Errorf("unable to read remote config: %v", err)
continue
}
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")
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 實例以供您的應用程序使用。每實例都有自己獨立的設置和配置值。每個實例可以從不同的配置文件,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 取一個更好的名字嗎?
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://blog.51cto.com/u_13599072/2072753