Go 每日一庫之完整配置解決方案:viper 源碼分析
包地址:github.com/spf13/viper
什么是Viper?
Viper是Go應(yīng)用程序的完整配置解決方案,包括12-Factor應(yīng)用程序。它旨在在應(yīng)用程序中工作,并可以處理所有類型的配置需求和格式。它支持:
設(shè)置默認(rèn)值 從JSON,TOML,YAML,HCL和Java屬性配置文件中讀取 實時觀看和重新讀取配置文件(可選) 從環(huán)境變量中讀取 從遠(yuǎn)程配置系統(tǒng)(etcd或Consul)讀取,并觀察變化 從命令行標(biāo)志讀取 從緩沖區(qū)讀取 設(shè)置顯式值
Viper可以被認(rèn)為是所有應(yīng)用程序配置需求的注冊表。
為何選擇Viper?
構(gòu)建現(xiàn)代應(yīng)用程序時,您不必?fù)?dān)心配置文件格式; 你想專注于構(gòu)建真棒軟件。Viper就是為此提供幫助的。
Viper為您做了以下事情:
以JSON,TOML,YAML,HCL或Java屬性格式查找,加載和解組配置文件。 提供一種機(jī)制來為不同的配置選項設(shè)置默認(rèn)值。 提供一種機(jī)制來為通過命令行標(biāo)志指定的選項設(shè)置覆蓋值。 提供別名系統(tǒng),輕松重命名參數(shù),而不會破壞現(xiàn)有代碼。 可以很容易地區(qū)分用戶提供命令行或配置文件與默認(rèn)值相同的時間。
Viper使用以下優(yōu)先順序。每個項目優(yōu)先于其下方的項目:
使用Viper設(shè)置默認(rèn)值
explicit call to Set flag env config key/value store default
viper.SetDefault("ContentDir",?"content")
viper.SetDefault("LayoutDir",?"layouts")
viper.SetDefault("Taxonomies",?
Viper讀取配置文件
Viper需要最少的配置,因此它知道在哪里查找配置文件。Viper支持JSON,TOML,YAML,HCL和Java Properties文件。Viper可以搜索多個路徑,但目前單個Viper實例僅支持單個配置文件。Viper不會默認(rèn)使用任何配置搜索路徑,而是將默認(rèn)值決定應(yīng)用于應(yīng)用程序。
以下是如何使用Viper搜索和讀取配置文件的示例。不需要任何特定路徑,但應(yīng)在預(yù)期配置文件的位置提供至少一個路徑。
viper.SetConfigName("config")?//?name?of?config?file?(without?extension)
viper.AddConfigPath("/etc/appname/")???//?path?to?look?for?the?config?file?in
viper.AddConfigPath("$HOME/.appname")??//?call?multiple?times?to?add?many?search?paths
viper.AddConfigPath(".")???????????????//?optionally?look?for?config?in?the?working?directory
err?:=?viper.ReadInConfig()?//?Find?and?read?the?config?file
if?err?!=?nil?{?//?Handle?errors?reading?the?config?file
?panic(fmt.Errorf("Fatal?error?config?file:?%s?\n",?err))
}
監(jiān)聽并重新讀取配置文件
Viper支持在運行時讓應(yīng)用程序?qū)崟r讀取配置文件。
需要重新啟動服務(wù)器以使配置生效的日子已經(jīng)一去不復(fù)返了,viper驅(qū)動的應(yīng)用程序可以在運行時讀取配置文件的更新,而不會錯過任何一個節(jié)拍。
只需告訴viper實例watchConfig即可。您可以選擇為Viper提供每次發(fā)生更改時運行的功能。
確保在調(diào)用之前添加所有configPath WatchConfig()
viper.WatchConfig()
viper.OnConfigChange(func(e?fsnotify.Event)?{
?fmt.Println("Config?file?changed:",?e.Name)
})
從io.Reader讀取配置
Viper預(yù)定義了許多配置源,例如文件,環(huán)境變量,標(biāo)志和遠(yuǎn)程K / V存儲,但您不受它們的約束。您還可以實現(xiàn)自己的必需配置源并將其提供給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")
Viper設(shè)置并覆蓋配置值
viper.Set("Verbose",?true)
viper.Set("LogFile",?LogFile)
Viper注冊和使用別名
別名允許多個鍵引用單個值
viper.RegisterAlias("loud",?"Verbose")
viper.Set("verbose",?true)?//?same?result?as?next?line
viper.Set("loud",?true)???//?same?result?as?prior?line
viper.GetBool("loud")?//?trueviper.GetBool("verbose")?//?true
Viper使用環(huán)境變量
Viper完全支持環(huán)境變量。有四種方法可以幫助使用ENV:
AutomaticEnv()
BindEnv(string...)?:?error
SetEnvPrefix(string)
SetEnvKeyReplacer(string...)?*strings.Replacer
BindEnv需要一個或兩個參數(shù)。第一個參數(shù)是鍵名,第二個是環(huán)境變量的名稱。環(huán)境變量的名稱區(qū)分大小寫。如果未提供ENV變量名,則Viper將自動假設(shè)密鑰名稱與ENV變量名稱匹配,但ENV變量為IN ALL CAPS。當(dāng)您明確提供ENV變量名稱時,它不會自動添加前綴。
使用ENV變量時要認(rèn)識到的一件重要事情是每次訪問時都會讀取該值。Viper在BindEnv調(diào)用時不會修復(fù)該值。
AutomaticEnv尤其是當(dāng)與結(jié)合了強(qiáng)大的幫手 SetEnvPrefix。調(diào)用時,Viper將在任何viper.Get請求發(fā)出時檢查環(huán)境變量。它將適用以下規(guī)則。它將檢查一個環(huán)境變量,其名稱與大寫的鍵匹配,并以EnvPrefix前綴。
SetEnvKeyReplacer允許您使用strings.Replacer對象重寫Env鍵到一定程度。如果要-在Get()調(diào)用中使用或使用某些內(nèi)容 ,但希望環(huán)境變量使用_分隔符,則此選項非常有用。可以在中找到使用它的示例viper_test.go
ENV實例
SetEnvPrefix("spf")??//將自動大寫
BindEnv("id")
os.Setenv("SPF_ID",?"13")?//?通常在應(yīng)用以外完成
id?:=?Get("id")?//?13
使用flag
Viper能夠綁定到flag。
就像BindEnv,在調(diào)用綁定方法時,不會設(shè)置該值。這意味著您可以盡早綁定,甚至可以在init()函數(shù)中綁定 。
對于單個標(biāo)志,該BindPFlag()方法提供此功能。
serverCmd.Flags().Int("port",?1138,?"Port?to?run?Application?server?on")
viper.BindPFlag("port",?serverCmd.Flags().Lookup("port"))
您還可以綁定一組現(xiàn)有的pflags(pflag.FlagSet)
pflag.Int("flagname",?1234,?"help?message?for?flagname")
pflag.Parse()
viper.BindPFlags(pflag.CommandLine)i?:=?viper.GetInt("flagname")
在Viper中使用pflag并不排除使用 標(biāo)準(zhǔn)庫中使用標(biāo)志包的其他包。pflag包可以通過導(dǎo)入這些標(biāo)志來處理為標(biāo)志包定義的標(biāo)志。這是通過調(diào)用名為AddGoFlagSet()的pflag包提供的便利函數(shù)來實現(xiàn)的。
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接口
如果您不使用,Viper提供兩個Go接口來綁定其他標(biāo)志系統(tǒng)Pflags
FlagValue代表一個標(biāo)志。這是一個關(guān)于如何實現(xiàn)此接口的非常簡單的示例:
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實現(xiàn)了這個接口,你可以告訴Viper綁定它:
viper.BindFlagValue("my-flag-name",?myFlag{})
遠(yuǎn)程key/value存儲
要在Viper中啟用遠(yuǎn)程支持,請對viper/remote 包進(jìn)行空白導(dǎo)入:
import?_?"github.com/spf13/viper/remote"
Viper將讀取key/value存儲(如etcd或Consul)中的路徑檢索的配置字符串(如JSON,TOML,YAML或HCL)。這些值優(yōu)先于默認(rèn)值,但會被從磁盤,標(biāo)志或環(huán)境變量檢索的配置值覆蓋。
Viper使用crypt從K / V存儲中檢索配置,這意味著您可以存儲加密的配置值,并在擁有正確的gpg密鑰環(huán)時自動解密。加密是可選的。
您可以將遠(yuǎn)程配置與本地配置結(jié)合使用,也可以獨立使用。
crypt有一個命令行幫助程序,您可以使用它來將配置放入K / V存儲區(qū)。crypt在http://127.0.0.1:4001上默認(rèn)為etcd 。
$?go?get?github.com/xordataexchange/crypt/bin/crypt
$?crypt?set?-plaintext?/config/hugo.json?/Users/hugo/settings/config.json
確認(rèn)您的值已設(shè)置:
$?crypt?get?-plaintext?/config/hugo.json
遠(yuǎn)程key/value存儲示例 - 未加密
viper.AddRemoteProvider("etcd",?"http://127.0.0.1:4001","/config/hugo.json")
viper.SetConfigType("json")?//因為字節(jié)流中沒有文件擴(kuò)展名,支持的擴(kuò)展名是“json”,“toml”,“yaml”,“yml”,“properties”,“props”,“prop”
err?:=?viper.ReadRemoteConfig()
遠(yuǎn)程key/value存儲示例 - 加密
viper.AddSecureRemoteProvider("etcd","http://127.0.0.1:4001","/config/hugo.json","/etc/secrets/mykeyring.gpg")
viper.SetConfigType("json")?//因為字節(jié)流中沒有文件擴(kuò)展名,支持的擴(kuò)展名是“json”,“toml”,“yaml”,“yml”,“properties”,“props”,“prop”
err?:=?viper.ReadRemoteConfig()
監(jiān)聽etcd中的變化 - 未加密
//或者,您可以創(chuàng)建一個新的viper實例
var?runtime_viper?=?viper.New()
runtime_viper.AddRemoteProvider("etcd",?"http://127.0.0.1:4001",?"/config/hugo.yml")
runtime_viper.SetConfigType("yaml")//?第一次從遠(yuǎn)程配置中讀取
err?:=?runtime_viper.ReadRemoteConfig()//解密配置
runtime_viper.Unmarshal(&runtime_conf)//?打開一個goroutine來永遠(yuǎn)監(jiān)聽遠(yuǎn)程變化
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
????}
????//將新配置解組到我們的運行時配置結(jié)構(gòu)中。您還可以使用通道
????//實現(xiàn)信號以通知系統(tǒng)更改
????runtime_viper.Unmarshal(&runtime_conf)
??}
}()
Viper獲取值
在Viper中,有幾種方法可以根據(jù)值的類型獲取值。存在以下功能和方法:
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
AllSettings()?:?map[string]interface{}
如果找不到,每個Get函數(shù)都將返回零值。IsSet()方法檢查給定密鑰是否存在。
實例:
viper.GetString("logfile")?//?case-insensitive?Setting?&?Gettingif
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")
這符合上面建立的優(yōu)先規(guī)則; 搜索路徑將在剩余的配置注冊表中級聯(lián),直到找到。
例如,給定此配置文件,都datastore.metric.host和 datastore.metric.port已經(jīng)定義(并且可以被覆蓋)。如果另外datastore.metric.protocol在默認(rèn)值中定義,Viper也會找到它。
但是,如果使用立即值datastore.metric覆蓋(通過標(biāo)志,環(huán)境變量,Set()方法,...),則所有子鍵 datastore.metric變?yōu)槲炊x,它們將被更高優(yōu)先級的配置級別“遮蔽”。
最后,如果存在與分隔的鍵路徑匹配的鍵,則將返回其值。例如
{
??"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"
提取sub-tree
例如
app:
??cache1:
????max-items:?100
????item-size:?64
??cache2:
????max-items:?200
????item-size:?80
#執(zhí)行后
subv?:=?viper.Sub("app.cache1")
#subv為
max-items:100
item-size:64
假設(shè)
func?NewCache(cfg?*Viper)?*Cache?{...}
它根據(jù)格式化為的配置信息創(chuàng)建緩存subv。現(xiàn)在可以輕松地分別創(chuàng)建這兩個緩存:
cfg1?:=?viper.Sub("app.cache1")
cache1?:=?NewCache(cfg1)
cfg2?:=?viper.Sub("app.cache2")
cache2?:=?NewCache(cfg2)
遍歷
您還可以選擇Unmarshaling all或特定值到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?configerr?:=?Unmarshal(&C)
if?err?!=?nil?{
??t.Fatalf("unable?to?decode?into?struct,?%v",?err)
}
轉(zhuǎn)為字符串
您可能需要將viper中保存的所有設(shè)置變?yōu)樽址皇菍⑺鼈儗懭胛募D梢允褂媚矚g的格式的marshaller和返回的配置AllSettings()。
import?(
?yaml?"gopkg.in/yaml.v2"
?//?...
)
func?yamlStringSettings()?string?{
?c?:=?viper.AllSettings()
?bs,?err?:=?yaml.Marshal(c)
?if?err?!=?nil?{
??t.Fatalf("unable?to?marshal?config?to?YAML:?%v",?err)
?}
?return?string(bs)
}
Viper or Vipers?
Viper隨時可以使用。開始使用Viper無需配置或初始化。由于大多數(shù)應(yīng)用程序都希望使用單個中央存儲庫進(jìn)行配置,因此viper軟件包提供了此功能。它類似于單身人士。
在上面的所有示例中,他們演示了使用viper的單例式方法。
使用多個Viper
您還可以創(chuàng)建許多不同的viper,以便在您的應(yīng)用程序中使用。每個都有自己獨特的配置和價值觀。每個都可以從不同的配置文件,鍵值存儲等中讀取.viper包支持的所有功能都被鏡像為viper上的方法。
例如
x?:=?viper.New()
y?:=?viper.New()
x.SetDefault("ContentDir",?"content")
y.SetDefault("ContentDir",?"foobar")
使用多viper時,用戶可以跟蹤不同的viper。
viper 的使用非常簡單,它需要很少的設(shè)置。設(shè)置文件名(SetConfigName)、配置類型(SetConfigType)和搜索路徑(AddConfigPath),然后調(diào)用ReadInConfig。viper會自動根據(jù)類型來讀取配置。使用時調(diào)用viper.Get方法獲取鍵值。
有幾點需要注意:
設(shè)置文件名時不要帶后綴; 搜索路徑可以設(shè)置多個,viper 會根據(jù)設(shè)置順序依次查找; viper 獲取值時使用 section.key的形式,即傳入嵌套的鍵名;默認(rèn)值可以調(diào)用 viper.SetDefault設(shè)置。
讀取鍵
viper 提供了多種形式的讀取方法。在上面的例子中,我們看到了Get方法的用法。Get方法返回一個interface{}的值,使用有所不便。
GetType系列方法可以返回指定類型的值。
其中,Type 可以為Bool/Float64/Int/String/Time/Duration/IntSlice/StringSlice。
但是請注意,如果指定的鍵不存在或類型不正確,GetType方法返回對應(yīng)類型的零值。
如果要判斷某個鍵是否存在,使用IsSet方法。
另外,GetStringMap和GetStringMapString直接以 map 返回某個鍵下面所有的鍵值對,前者返回map[string]interface{},后者返回map[string]string。AllSettings以map[string]interface{}返回所有設(shè)置。
Unmarshal
viper 支持將配置Unmarshal到一個結(jié)構(gòu)體中,為結(jié)構(gòu)體中的對應(yīng)字段賦值。
保存配置
有時候,我們想要將程序中生成的配置,或者所做的修改保存下來。viper 提供了接口!
WriteConfig:將當(dāng)前的 viper 配置寫到預(yù)定義路徑,如果沒有預(yù)定義路徑,返回錯誤。將會覆蓋當(dāng)前配置;SafeWriteConfig:與上面功能一樣,但是如果配置文件存在,則不覆蓋;WriteConfigAs:保存配置到指定路徑,如果文件存在,則覆蓋;SafeWriteConfig:與上面功能一樣,但是入股配置文件存在,則不覆蓋。
viper的代碼很簡潔,配置讀取的思路是
根據(jù)用戶設(shè)置的目錄加載文件,查找用戶設(shè)置的文件類型,如果沒有設(shè)置類型,則根據(jù)配置文件名的擴(kuò)展來確定類型,然后將配置文件unmarshal到一個map[string]interface{}中,unmarshal 方法根據(jù)不同的文件類型,使用不同的解析方法。
推薦閱讀
