Viper中文教程
Viper是適用於Go應用程序的完整配置解決方案。它被設計用於在應用程序中工作,並且可以處理所有類型的配置需求和格式。
安裝
go get github.com/spf13/viper
什么是Viper?
Viper是適用於Go應用程序(包括Twelve-Factor App
)的完整配置解決方案。它被設計用於在應用程序中工作,並且可以處理所有類型的配置需求和格式。它支持以下特性:
- 設置默認值
- 從
JSON
、TOML
、YAML
、HCL
、envfile
和Java properties
格式的配置文件讀取配置信息 - 實時監控和重新讀取配置文件(可選)
- 從環境變量中讀取
- 從遠程配置系統(etcd或Consul)讀取並監控配置變化
- 從命令行參數讀取配置
- 從buffer讀取配置
- 顯式配置值
為什么選擇Viper?
在構建現代應用程序時,你無需擔心配置文件格式;你想要專注於構建出色的軟件。Viper的出現就是為了在這方面幫助你的。
Viper能夠為你執行下列操作:
- 查找、加載和反序列化
JSON
、TOML
、YAML
、HCL
、envfile
和Java properties
格式的配置文件。 - 提供一種機制為你的不同配置選項設置默認值。
- 提供一種機制來通過命令行參數覆蓋指定選項的值。
- 提供別名系統,以便在不破壞現有代碼的情況下輕松重命名參數。
- 當用戶提供了與默認值相同的命令行或配置文件時,可以很容易地分辨出它們之間的區別。
Viper會按照下面的優先級。每個項目的優先級都高於它下面的項目:
- 顯示調用
Set
設置值 - 命令行參數(flag)
- 環境變量
- 配置文件
- key/value存儲
- 默認值
重要: 目前Viper配置的鍵(Key)是大小寫不敏感的。目前正在討論是否將這一選項設為可選。
把值存入Viper
建立默認值
一個好的配置系統應該支持默認值。鍵不需要默認值,但如果沒有通過配置文件、環境變量、遠程配置或命令行標志(flag)設置鍵,則默認值非常有用。
例如:
viper.SetDefault("ContentDir", "content")
viper.SetDefault("LayoutDir", "layouts")
viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "categories"})
讀取配置文件
Viper需要最少知道在哪里查找配置文件的配置。Viper支持JSON
、TOML
、YAML
、HCL
、envfile
和Java properties
格式的配置文件。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 { // 處理讀取配置文件的錯誤
panic(fmt.Errorf("Fatal error config file: %s \n", err))
}
你可以像下面這樣處理找不到配置文件的特定情況:
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// 配置文件未找到錯誤;如果需要可以忽略
} else {
// 配置文件被找到,但產生了另外的錯誤
}
}
// 配置文件找到並成功解析
注意[自1.6起]: 你也可以有不帶擴展名的文件,並以編程方式指定其格式。對於位於用戶$HOME
目錄中的配置文件沒有任何擴展名,如.bashrc
。
寫入配置文件
從配置文件中讀取配置文件是有用的,但是有時你想要存儲在運行時所做的所有修改。為此,可以使用下面一組命令,每個命令都有自己的用途:
- WriteConfig - 將當前的
viper
配置寫入預定義的路徑並覆蓋(如果存在的話)。如果沒有預定義的路徑,則報錯。 - SafeWriteConfig - 將當前的
viper
配置寫入預定義的路徑。如果沒有預定義的路徑,則報錯。如果存在,將不會覆蓋當前的配置文件。 - WriteConfigAs - 將當前的
viper
配置寫入給定的文件路徑。將覆蓋給定的文件(如果它存在的話)。 - SafeWriteConfigAs - 將當前的
viper
配置寫入給定的文件路徑。不會覆蓋給定的文件(如果它存在的話)。
根據經驗,標記為safe
的所有方法都不會覆蓋任何文件,而是直接創建(如果不存在),而默認行為是創建或截斷。
一個小示例:
viper.WriteConfig() // 將當前配置寫入“viper.AddConfigPath()”和“viper.SetConfigName”設置的預定義路徑
viper.SafeWriteConfig()
viper.WriteConfigAs("/path/to/my/.config")
viper.SafeWriteConfigAs("/path/to/my/.config") // 因為該配置文件寫入過,所以會報錯
viper.SafeWriteConfigAs("/path/to/my/.other_config")
監控並重新讀取配置文件
Viper支持在運行時實時讀取配置文件的功能。
需要重新啟動服務器以使配置生效的日子已經一去不復返了,viper驅動的應用程序可以在運行時讀取配置文件的更新,而不會錯過任何消息。
只需告訴viper實例watchConfig。可選地,你可以為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") // 或者 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
`)
viper.ReadConfig(bytes.NewBuffer(yamlExample))
viper.Get("name") // 這里會得到 "steve"
覆蓋設置
這些可能來自命令行標志,也可能來自你自己的應用程序邏輯。
viper.Set("Verbose", true)
viper.Set("LogFile", LogFile)
注冊和使用別名
別名允許多個鍵引用單個值
viper.RegisterAlias("loud", "Verbose") // 注冊別名(此處loud和Verbose建立了別名)
viper.Set("verbose", true) // 結果與下一行相同
viper.Set("loud", true) // 結果與前一行相同
viper.GetBool("loud") // true
viper.GetBool("verbose") // true
使用環境變量
Viper完全支持環境變量。這使Twelve-Factor App
開箱即用。有五種方法可以幫助與ENV協作:
AutomaticEnv()
BindEnv(string...) : error
SetEnvPrefix(string)
SetEnvKeyReplacer(string...) *strings.Replacer
AllowEmptyEnv(bool)
使用ENV變量時,務必要意識到Viper將ENV變量視為區分大小寫。
Viper提供了一種機制來確保ENV變量是惟一的。通過使用SetEnvPrefix
,你可以告訴Viper在讀取環境變量時使用前綴。BindEnv
和AutomaticEnv
都將使用這個前綴。
BindEnv
使用一個或兩個參數。第一個參數是鍵名稱,第二個是環境變量的名稱。環境變量的名稱區分大小寫。如果沒有提供ENV變量名,那么Viper將自動假設ENV變量與以下格式匹配:前綴+ "_" +鍵名全部大寫。當你顯式提供ENV變量名(第二個參數)時,它 不會 自動添加前綴。例如,如果第二個參數是“id”,Viper將查找環境變量“ID”。
在使用ENV變量時,需要注意的一件重要事情是,每次訪問該值時都將讀取它。Viper在調用BindEnv
時不固定該值。
AutomaticEnv
是一個強大的助手,尤其是與SetEnvPrefix
結合使用時。調用時,Viper會在發出viper.Get
請求時隨時檢查環境變量。它將應用以下規則。它將檢查環境變量的名稱是否與鍵匹配(如果設置了EnvPrefix
)。
SetEnvKeyReplacer
允許你使用strings.Replacer
對象在一定程度上重寫 Env 鍵。如果你希望在Get()
調用中使用-
或者其他什么符號,但是環境變量里使用_
分隔符,那么這個功能是非常有用的。可以在viper_test.go
中找到它的使用示例。
或者,你可以使用帶有NewWithOptions
工廠函數的EnvKeyReplacer
。與SetEnvKeyReplacer
不同,它接受StringReplacer
接口,允許你編寫自定義字符串替換邏輯。
默認情況下,空環境變量被認為是未設置的,並將返回到下一個配置源。若要將空環境變量視為已設置,請使用AllowEmptyEnv
方法。
Env 示例:
SetEnvPrefix("spf") // 將自動轉為大寫
BindEnv("id")
os.Setenv("SPF_ID", "13") // 通常是在應用程序之外完成的
id := Get("id") // 13
使用Flags
Viper 具有綁定到標志的能力。具體來說,Viper支持Cobra庫中使用的Pflag
。
與BindEnv
類似,該值不是在調用綁定方法時設置的,而是在訪問該方法時設置的。這意味着你可以根據需要盡早進行綁定,即使在init()
函數中也是如此。
對於單個標志,BindPFlag()
方法提供此功能。
例如:
serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))
你還可以綁定一組現有的pflags (pflag.FlagSet):
舉個例子:
pflag.Int("flagname", 1234, "help message for flagname")
pflag.Parse()
viper.BindPFlags(pflag.CommandLine)
i := viper.GetInt("flagname") // 從viper而不是從pflag檢索值
在 Viper 中使用 pflag 並不阻礙其他包中使用標准庫中的 flag 包。pflag 包可以通過導入這些 flags 來處理flag包定義的flags。這是通過調用pflag包提供的便利函數AddGoFlagSet()
來實現的。
例如:
package main
import (
"flag"
"github.com/spf13/pflag"
)
func main() {
// 使用標准庫 "flag" 包
flag.Int("flagname", 1234, "help message for flagname")
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
pflag.Parse()
viper.BindPFlags(pflag.CommandLine)
i := viper.GetInt("flagname") // 從 viper 檢索值
...
}
flag接口
如果你不使用Pflag
,Viper 提供了兩個Go接口來綁定其他 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" }
一旦你的 flag 實現了這個接口,你可以很方便地告訴Viper綁定它:
viper.BindFlagValue("my-flag-name", myFlag{})
FlagValueSet
代表一組 flags 。這是一個關於如何實現這個接口的非常簡單的例子:
type myFlagSet struct {
flags []myFlag
}
func (f myFlagSet) VisitAll(fn func(FlagValue)) {
for _, flag := range flags {
fn(flag)
}
}
一旦你的flag set實現了這個接口,你就可以很方便地告訴Viper綁定它:
fSet := myFlagSet{
flags: []myFlag{myFlag{}, myFlag{}},
}
viper.BindFlagValues("my-flags", fSet)
遠程Key/Value存儲支持
在Viper中啟用遠程支持,需要在代碼中匿名導入viper/remote
這個包。
import _ "github.com/spf13/viper/remote"
Viper將讀取從Key/Value存儲(例如etcd或Consul)中的路徑檢索到的配置字符串(如JSON
、TOML
、YAML
、HCL
、envfile
和Java properties
格式)。這些值的優先級高於默認值,但是會被從磁盤、flag或環境變量檢索到的配置值覆蓋。(譯注:也就是說Viper加載配置值的優先級為:磁盤上的配置文件>命令行標志位>環境變量>遠程Key/Value存儲>默認值。)
Viper使用crypt從K/V存儲中檢索配置,這意味着如果你有正確的gpg密匙,你可以將配置值加密存儲並自動解密。加密是可選的。
你可以將遠程配置與本地配置結合使用,也可以獨立使用。
crypt
有一個命令行助手,你可以使用它將配置放入K/V存儲中。crypt
默認使用在http://127.0.0.1:4001的etcd。
$ go get github.com/bketelsen/crypt/bin/crypt
$ crypt set -plaintext /config/hugo.json /Users/hugo/settings/config.json
確認值已經設置:
$ crypt get -plaintext /config/hugo.json
有關如何設置加密值或如何使用Consul的示例,請參見crypt
文檔。
遠程Key/Value存儲示例-未加密
etcd
viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001","/config/hugo.json")
viper.SetConfigType("json") // 因為在字節流中沒有文件擴展名,所以這里需要設置下類型。支持的擴展名有 "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv"
err := viper.ReadRemoteConfig()
Consul
你需要 Consul Key/Value存儲中設置一個Key保存包含所需配置的JSON值。例如,創建一個keyMY_CONSUL_KEY
將下面的值存入Consul key/value 存儲:
{
"port": 8080,
"hostname": "liwenzhou.com"
}
viper.AddRemoteProvider("consul", "localhost:8500", "MY_CONSUL_KEY")
viper.SetConfigType("json") // 需要顯示設置成json
err := viper.ReadRemoteConfig()
fmt.Println(viper.Get("port")) // 8080
fmt.Println(viper.Get("hostname")) // liwenzhou.com
Firestore
viper.AddRemoteProvider("firestore", "google-cloud-project-id", "collection/document")
viper.SetConfigType("json") // 配置的格式: "json", "toml", "yaml", "yml"
err := viper.ReadRemoteConfig()
當然,你也可以使用SecureRemoteProvider
。
遠程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", "env", "dotenv"
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", "env", "dotenv"
// 第一次從遠程讀取配置
err := runtime_viper.ReadRemoteConfig()
// 反序列化
runtime_viper.Unmarshal(&runtime_conf)
// 開啟一個單獨的goroutine一直監控遠端的變更
go func(){
for {
time.Sleep(time.Second * 5) // 每次請求后延遲一下
// 目前只測試了etcd支持
err := runtime_viper.WatchRemoteConfig()
if err != nil {
log.Errorf("unable to read remote config: %v", err)
continue
}
// 將新配置反序列化到我們運行時的配置結構體中。你還可以借助channel實現一個通知系統更改的信號
runtime_viper.Unmarshal(&runtime_conf)
}
}()
從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{}
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{}
需要認識到的一件重要事情是,每一個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") // (返回 "127.0.0.1")
這遵守上面建立的優先規則;搜索路徑將遍歷其余配置注冊表,直到找到為止。(譯注:因為Viper支持從多種配置來源,例如磁盤上的配置文件>命令行標志位>環境變量>遠程Key/Value存儲>默認值,我們在查找一個配置的時候如果在當前配置源中沒找到,就會繼續從后續的配置源查找,直到找到為止。)
例如,在給定此配置文件的情況下,datastore.metric.host
和datastore.metric.port
均已定義(並且可以被覆蓋)。如果另外在默認值中定義了datastore.metric.protocol
,Viper也會找到它。
然而,如果datastore.metric
被直接賦值覆蓋(被flag,環境變量,set()
方法等等...),那么datastore.metric
的所有子鍵都將變為未定義狀態,它們被高優先級配置級別“遮蔽”(shadowed)了。
最后,如果存在與分隔的鍵路徑匹配的鍵,則返回其值。例如:
{
"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") // 返回 "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 {...}
它基於subv
格式的配置信息創建緩存。現在,可以輕松地分別創建這兩個緩存,如下所示:
cfg1 := viper.Sub("app.cache1")
cache1 := NewCache(cfg1)
cfg2 := viper.Sub("app.cache2")
cache2 := NewCache(cfg2)
反序列化
你還可以選擇將所有或特定的值解析到結構體、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
tag。
序列化成字符串
你可能需要將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包提供了這個功能。它類似於單例模式。
在上面的所有示例中,它們都以其單例風格的方法演示了如何使用viper。
使用多個viper實例
你還可以在應用程序中創建許多不同的viper實例。每個都有自己獨特的一組配置和值。每個人都可以從不同的配置文件,key value存儲區等讀取數據。每個都可以從不同的配置文件、鍵值存儲等中讀取。viper包支持的所有功能都被鏡像為viper實例的方法。
例如:
x := viper.New()
y := viper.New()
x.SetDefault("ContentDir", "content")
y.SetDefault("ContentDir", "foobar")
//...
當使用多個viper實例時,由用戶來管理不同的viper實例。
以上內容翻譯自Viper官方README文檔,為方便理解部分內容稍作修改。
使用Viper示例
這里用一個demo演示如何在gin框架搭建的web項目中使用viper。
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
)
func main() {
viper.SetConfigName("config") // 指定配置文件名稱(不需要帶后綴)
viper.SetConfigType("yaml") // 指定配置文件類型
viper.AddConfigPath("./conf/") // 指定查找配置文件的路徑(這里使用相對路徑)
err := viper.ReadInConfig() // 讀取配置信息
if err != nil { // 讀取配置信息失敗
panic(fmt.Errorf("Fatal error config file: %s \n", err))
}
r := gin.Default()
if err := r.Run(fmt.Sprintf(":%d", viper.Get("port"))); err != nil {
panic(err)
}
}
./conf/config.yaml
文件內容如下:
port: 8123