Go項(xiàng)目配置管理神器之viper的介紹與使用詳解
1. viper的介紹
viper是go一個(gè)強(qiáng)大的流行的配置解決方案的庫。viper是spf13的另外一個(gè)重量級(jí)庫。有大量項(xiàng)目都使用該庫,比如hugo, docker等。 它基本上可以處理所有類型的配置需求和格式, viper支持功能
- 設(shè)置默認(rèn)配置
- 支持各種配置文件,如JSON,TOML, YAML, HCL, envfile和Java屬性配置文件
- 支持監(jiān)聽文件變化以及重新讀取配置
- 支持從環(huán)境變量讀取配置
- 支持從遠(yuǎn)程配置系統(tǒng)(etcd或Consul)讀取配置,并能監(jiān)聽遠(yuǎn)程配置修改
- 支持從命令行標(biāo)志Flag讀取配置,比如搭配cobra使用
- 支持讀取緩沖區(qū)數(shù)據(jù)
Viper主要為我們做以下工作:
- 查找、加載和解組JSON、TOML、YAML、HCL、INI、envfile或Java屬性格式的配置文件。
- 提供一種機(jī)制來為不同的配置選項(xiàng)設(shè)置默認(rèn)值。
- 提供一種機(jī)制來為通過命令行標(biāo)志指定的選項(xiàng)設(shè)置覆蓋值。
- 提供別名系統(tǒng),以便在不破壞現(xiàn)有代碼的情況下輕松重命名參數(shù)。
- 當(dāng)用戶提供了與默認(rèn)值相同的命令行或配置文件時(shí),很容易區(qū)分它們。
viepr的安裝很簡(jiǎn)單,直接再工程中使用go get命令安裝即可
$ go get github.com/spf13/viper
2. viper的使用
2.1 Viper對(duì)象的創(chuàng)建
Viper的是viper庫的主要實(shí)現(xiàn)對(duì)象, viper提供了下面的方法可以獲取Viper實(shí)例:
func GetViper() *Viper func New() *Viper func NewWithOptions(opts ...Option) *Viper func Sub(key string) *Viper
使用viper.GetViper()獲取的為全局的Viper實(shí)例對(duì)象,默認(rèn)使用viper包使用也是該全局Viper實(shí)例。查看viper的源碼,可以看到viper默認(rèn)提供了一個(gè)全局的Viper實(shí)例:
var v *Viper func init() { v = New() } // New returns an initialized Viper instance. func New() *Viper { v := new(Viper) v.keyDelim = "." v.configName = "config" v.configPermissions = os.FileMode(0o644) v.fs = afero.NewOsFs() v.config = make(map[string]interface{}) v.override = make(map[string]interface{}) v.defaults = make(map[string]interface{}) v.kvstore = make(map[string]interface{}) v.pflags = make(map[string]FlagValue) v.env = make(map[string][]string) v.aliases = make(map[string]string) v.typeByDefValue = false v.logger = jwwLogger{} v.resetEncoding() return v }
New和NewWithOptions為我們提供了創(chuàng)建實(shí)例的方法
func New1() *viper.Viper { return viper.New() } func New2() *viper.Viper { return viper.NewWithOptions() }
Sub為我們讀取子配置項(xiàng)提供了一個(gè)新的實(shí)例Viper
v := viper.Sub("db") url := v.Get("url") log.Printf("mysql url:%s\n", url)
2.2 預(yù)設(shè)一些默認(rèn)配置
viper.SetDefault("ContentDir", "content") viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "categories"}) viper.SetDefault("redis.port", 6379) viper.SetDefault("mysql.url", "root:root@tcp(127.0.0.1:3306)/stock?charset=utf8mb4&parseTime=True&loc=Local")
2.3 從命令行工具的選項(xiàng)參數(shù)Flags讀取
viper主要提供了以下四個(gè)方法,可以綁定行參數(shù)的輸出的選項(xiàng)值:
func (v *Viper) BindFlagValue(key string, flag FlagValue) error func (v *Viper) BindFlagValues(flags FlagValueSet) (err error) func (v *Viper) BindPFlag(key string, flag *pflag.Flag) error func (v *Viper) BindPFlags(flags *pflag.FlagSet) error
這里我們主要結(jié)合之前講的cobra庫中的pflag來講解一下viper對(duì)Flags選項(xiàng)參數(shù)的綁定。
在cobra中,我們主要通過cobra.Command來組織不同的命令和子命令,這里我們我通過在root根命令來做測(cè)試。代碼如下:
func init(){ rootCmd.Flags().String("author", "YOUR NAME", "Author name for copyright attribution") rootCmd.Flags().String("email", "YOUR EMAIL", "Author email for contact") // 綁定多個(gè)key-value值 viper.BindPFlags(rootCmd.Flags()) // 單個(gè)綁定不同的key viper.BindPFlag("author", rootCmd.Flags().Lookup("author")) viper.BindPFlag("email", rootCmd.Flags().Lookup("email")) rootCmd.AddCommand(version.VersionCmd) }
在cobra的命令的run回調(diào)方法中,我們通過viper的來獲取輸入的選項(xiàng)值
func run(){ fmt.Println("go root cmd run") fmt.Println(viper.GetString("author")) fmt.Println(viper.GetString("email")) }
啟動(dòng)飲用,傳入?yún)?shù)測(cè)試一下:
go run main.go --author ckeen --email ck@gmail.com
查看一下打印結(jié)果,可以看到從viper成功獲取到以flag傳入的參數(shù)值:
? cli git:(master) ? go run main.go --author keen --email ck@gmail.com go root cmd run ckeen ck@gmail.com
2.4 從環(huán)境變量讀取
viper支持環(huán)境變量的函數(shù):
func (v *Viper) AutomaticEnv() // 開啟綁定環(huán)境變量 func (v *Viper) BindEnv(input ...string) error // 綁定系統(tǒng)中某個(gè)環(huán)境變量 func (v *Viper) SetEnvKeyReplacer(r *strings.Replacer) func (v *Viper) SetEnvPrefix(in string)
使用方法:
使用AutomaticEnv()開啟綁定環(huán)境變量,沒開啟的時(shí)候不會(huì)從環(huán)境變量獲取,開啟后可以獲取環(huán)境變量的值。如果不想開啟所有環(huán)境變量值,可以使用BindEnv(input …string)方法綁定單個(gè)環(huán)境變量的綁定, 那么只有該綁定的環(huán)境變量的key才能獲取到值
綁定環(huán)境變量后,可以使用正常的Get方法來獲取變量值,示例代碼如下:
func testEnv(){ v := New1() os.Setenv("CK_HOME","123") os.Setenv("CK_NAME","ckeen") v.AutomaticEnv() //v.BindEnv("SHELL") v.AllowEmptyEnv(true) log.Printf("os env:%+v\n", os.Environ()) log.Printf("env: %+v\n", v.Get("HOME")) log.Printf("env: %+v\n", v.Get("SHELL")) v.SetEnvPrefix("CK") log.Printf("ck-home: %+v\n", v.Get("HOME")) log.Printf("ck-email: %+v\n", v.Get("NAME")) }
還可以通過SetEnvPrefix()方法設(shè)置環(huán)境變量前綴, 前綴和Key之間用下劃線分割
2.5 從配置文件讀取
下面我們看一下操作實(shí)例, 先看我們的配置文件app.yml文件:
app: name: viper-test mode: dev db: mysql: url: "root:root@tcp(127.0.0.1:3306)/stock?charset=utf8mb4&parseTime=True&loc=Local" redis: host: 127.0.0.1 port: 6067 db: 0 passwd: 123456
初始化配置
func InitConfig() (*viper.Viper, error) { v := viper.New() v.AddConfigPath(".") // 添加配置文件搜索路徑,點(diǎn)號(hào)為當(dāng)前目錄 v.AddConfigPath("./configs") // 添加多個(gè)搜索目錄 v.SetConfigType("yaml") // 如果配置文件沒有后綴,可以不用配置 v.SetConfigName("app.yml") // 文件名,沒有后綴 // v.SetConfigFile("configs/app.yml") // 讀取配置文件 if err := v.ReadInConfig(); err == nil { log.Printf("use config file -> %s\n", v.ConfigFileUsed()) } else { return nil,err } return v, nil }
首先這里我們添加一個(gè)配置文件搜索路徑,點(diǎn)號(hào)表示當(dāng)前路徑,搜索路徑可以添加多個(gè)然后設(shè)置了配置文件類型,這里我們?cè)O(shè)置文件類型為yaml,
接著我們?cè)O(shè)置了配置文件名稱,這個(gè)文件可以從配置的搜索路徑從查找。
最后我們通過提供的ReadInConfig()函數(shù)讀取配置文件
讀取配置文件
// 通過.號(hào)來區(qū)分不同層級(jí),來獲取配置值 log.Printf("app.mode=%s\n", v.Get("app.mode")) log.Printf("db.mysql.url=%s\n", v.Get("db.mysql.url")) log.Printf("db.redis.host=%s\n", v.GetString("db.redis.host")) log.Printf("db.redis.port=%d\n", v.GetInt("db.redis.port")) // 使用Sub獲取子配置,然后獲取配置值 v2 := v.Sub("db") log.Printf("db.mysql.url:%s\n", v2.Sub("mysql").GetString("url")) log.Printf("db.redis.host:%s\n", v2.Sub("redis").GetString("host")) log.Printf("db.redis.port:%s\n", v2.Sub("redis").GetInt("port"))
viper還提供了如下獲取類型獲取配置項(xiàng)值:
注: 其中重要的一個(gè)函數(shù)IsSet可以用來判斷某個(gè)key是否被設(shè)置
2.6 從遠(yuǎn)程key/value存儲(chǔ)讀取
在Viper中啟用遠(yuǎn)程支持,需要在代碼中匿名導(dǎo)入viper/remote
這個(gè)包。
_ "github.com/spf13/viper/remote"
Viper將讀取從Key/Value存儲(chǔ)中的路徑檢索到的配置字符串(如JSON
、TOML
、YAML
格式)。viper目前支持Consul/Etcd/firestore三種Key/Value的存儲(chǔ)系統(tǒng)。下面我來演示從etcd讀取配置:
首先我們安裝crypt的工具
go get github.com/bketelsen/crypt/bin/crypt
使用crypt的命令,將app.yml的文件添加到detcd
crypt set --endpoint=http://127.0.0.1:2379 -plaintext /config/app.yml /Users/ckeen/Documents/code/gosource/go-awesome/go-samples/viper/configs/app.yml
添加viper的操作遠(yuǎn)程資源的配置
_ "github.com/spf13/viper/remote"
實(shí)現(xiàn)從遠(yuǎn)程讀取配置
func InitConfigFromRemote() (*viper.Viper,error) { v := viper.New() // 遠(yuǎn)程配置 v.AddRemoteProvider("etcd","http://127.0.0.1:2379","config/app.yml") //v.SetConfigType("json") v.SetConfigFile("app.yml") v.SetConfigType("yml") if err := v.ReadRemoteConfig(); err == nil { log.Printf("use config file -> %s\n", v.ConfigFileUsed()) } else { return nil, err } return v, nil } func main(){ v, err := InitConfigFromRemote() if err != nil { log.Printf("read remote error:%+v\n") } log.Printf("remote read app.mode=%+v\n", v.GetString("app.mode")) log.Printf("remote read db.mysql.url=%+v\n", v.GetString("db.mysql.url")) }
測(cè)試打印結(jié)果
2.7 監(jiān)聽配置變化
viper提供如下兩種監(jiān)聽配置的函數(shù),一個(gè)是本地的監(jiān)聽和一個(gè)遠(yuǎn)程監(jiān)聽的:
func (v *Viper) WatchConfig() func (v *Viper) WatchRemoteConfig() error func (v *Viper) WatchRemoteConfigOnChannel() error
我們主要看一下監(jiān)聽本地文件變更的示例
v, err := InitConfig() if err != nil { log.Fatalf("viper讀取失敗, error:%+v\n",err) } // 監(jiān)聽到文件變化后的回調(diào) v.OnConfigChange(func(e fsnotify.Event) { fmt.Println("Config file changed:", e.Name) fmt.Println(v.Get("db.redis.passwd")) }) v.WatchConfig() // 阻塞進(jìn)程退出 time.Sleep(time.Duration(1000000) * time.Second)
我們使用前面的InitConfig()方法來初始化本地文件讀取配置,然后設(shè)定了監(jiān)聽函數(shù),最后使用WatchConfig()開啟本地文件監(jiān)聽。
當(dāng)我們修改本地配置configs/app.yml的db.redis.passwd的值,然后保存后,我們可以看到控制臺(tái)有打印最新修改后的值,不要我們重新去獲取。
2.8 寫入配置到文件
viper提供了如下四個(gè)寫入配置文件發(fā)方法
func (v *Viper) SafeWriteConfig() error func (v *Viper) SafeWriteConfigAs(filename string) error func (v *Viper) WriteConfig() error func (v *Viper) WriteConfigAs(filename string) error
使用SafeWriteConfig()和WriteConfig()時(shí),可以先設(shè)定SetConfigFile()設(shè)定配置文件的路徑。配置寫入示例:
v := New1() v.SetConfigFile("./hello.yml") log.Printf("config path:%+v\n", v.ConfigFileUsed()) v.SetDefault("author","CKeen") v.SetDefault("email", "ck@gmail.com") v.Set("hello", "foo") v.Set("slice", []string {"slice1","slice2","slice3"}) v.SetDefault("test.web", "https://ckeen.cn") v.WriteConfig() //v.WriteConfigAs("./hello.yml")
如果使用SafeWriteConfigAs()或者WriteConfigAs()方法,則直接傳入配置文件路徑即可。
3. 源碼分析--配置讀取的順序
通過上面的示例我們知道,viper讀取配置主要通過一系列Get方法來實(shí)現(xiàn),我們從Get方法跳轉(zhuǎn)到源碼可以發(fā)現(xiàn), 主要獲取的配置值的為find方法, 方法實(shí)現(xiàn)如下:
func (v *Viper) find(lcaseKey string, flagDefault bool) interface{} { var ( val interface{} exists bool path = strings.Split(lcaseKey, v.keyDelim) nested = len(path) > 1 ) // compute the path through the nested maps to the nested value if nested && v.isPathShadowedInDeepMap(path, castMapStringToMapInterface(v.aliases)) != "" { return nil } // if the requested key is an alias, then return the proper key lcaseKey = v.realKey(lcaseKey) path = strings.Split(lcaseKey, v.keyDelim) nested = len(path) > 1 // Set() override first val = v.searchMap(v.override, path) if val != nil { return val } if nested && v.isPathShadowedInDeepMap(path, v.override) != "" { return nil } // PFlag override next flag, exists := v.pflags[lcaseKey] if exists && flag.HasChanged() { switch flag.ValueType() { case "int", "int8", "int16", "int32", "int64": return cast.ToInt(flag.ValueString()) case "bool": return cast.ToBool(flag.ValueString()) case "stringSlice", "stringArray": s := strings.TrimPrefix(flag.ValueString(), "[") s = strings.TrimSuffix(s, "]") res, _ := readAsCSV(s) return res case "intSlice": s := strings.TrimPrefix(flag.ValueString(), "[") s = strings.TrimSuffix(s, "]") res, _ := readAsCSV(s) return cast.ToIntSlice(res) case "stringToString": return stringToStringConv(flag.ValueString()) default: return flag.ValueString() } } if nested && v.isPathShadowedInFlatMap(path, v.pflags) != "" { return nil } // Env override next if v.automaticEnvApplied { // even if it hasn't been registered, if automaticEnv is used, // check any Get request if val, ok := v.getEnv(v.mergeWithEnvPrefix(lcaseKey)); ok { return val } if nested && v.isPathShadowedInAutoEnv(path) != "" { return nil } } envkeys, exists := v.env[lcaseKey] if exists { for _, envkey := range envkeys { if val, ok := v.getEnv(envkey); ok { return val } } } if nested && v.isPathShadowedInFlatMap(path, v.env) != "" { return nil } // Config file next val = v.searchIndexableWithPathPrefixes(v.config, path) if val != nil { return val } if nested && v.isPathShadowedInDeepMap(path, v.config) != "" { return nil } // K/V store next val = v.searchMap(v.kvstore, path) if val != nil { return val } if nested && v.isPathShadowedInDeepMap(path, v.kvstore) != "" { return nil } // Default next val = v.searchMap(v.defaults, path) if val != nil { return val } if nested && v.isPathShadowedInDeepMap(path, v.defaults) != "" { return nil } if flagDefault { // last chance: if no value is found and a flag does exist for the key, // get the flag's default value even if the flag's value has not been set. if flag, exists := v.pflags[lcaseKey]; exists { switch flag.ValueType() { case "int", "int8", "int16", "int32", "int64": return cast.ToInt(flag.ValueString()) case "bool": return cast.ToBool(flag.ValueString()) case "stringSlice", "stringArray": s := strings.TrimPrefix(flag.ValueString(), "[") s = strings.TrimSuffix(s, "]") res, _ := readAsCSV(s) return res case "intSlice": s := strings.TrimPrefix(flag.ValueString(), "[") s = strings.TrimSuffix(s, "]") res, _ := readAsCSV(s) return cast.ToIntSlice(res) case "stringToString": return stringToStringConv(flag.ValueString()) default: return flag.ValueString() } } // last item, no need to check shadowing } return nil }
通過源碼,我們可以知道viper讀取配置的優(yōu)先級(jí)順序:alias別名 > 調(diào)用Set設(shè)置 > flag > env > config > key/value store > default
還有一個(gè)注意點(diǎn):viper配置鍵不區(qū)分大小寫,因?yàn)関iper內(nèi)部對(duì)key統(tǒng)一轉(zhuǎn)為了小寫。
4. 參考資料
viper的包地址:viper package - github.com/spf13/viper - Go Packages
viper的github地址: GitHub - spf13/viper: Go configuration with fangs
總結(jié)
到此這篇關(guān)于Go項(xiàng)目配置管理神器之viper的介紹與使用的文章就介紹到這了,更多相關(guān)Go配置管理神器viper使用內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
GoLang協(xié)程庫libtask學(xué)習(xí)筆記
libtask一個(gè)C語言的協(xié)程庫,是go語言的前身很早期的原型. 測(cè)試機(jī)器是我的mac air 安裝的centos虛擬機(jī)(只有一個(gè)核), 代碼沒有采用任何優(yōu)化,只是使用默認(rèn)配置2022-12-12Golang實(shí)現(xiàn)多存儲(chǔ)驅(qū)動(dòng)設(shè)計(jì)SDK案例
這篇文章主要介紹了Golang實(shí)現(xiàn)多存儲(chǔ)驅(qū)動(dòng)設(shè)計(jì)SDK案例,Gocache是一個(gè)基于Go語言編寫的多存儲(chǔ)驅(qū)動(dòng)的緩存擴(kuò)展組件,更多具體內(nèi)容感興趣的小伙伴可以參考一下2022-09-09GoFrame框架garray對(duì)比PHP的array優(yōu)勢(shì)
這篇文章主要為大家介紹了GoFrame框架garray對(duì)比PHP的array優(yōu)勢(shì)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06Golang?HTTP服務(wù)超時(shí)控制實(shí)現(xiàn)原理分析
這篇文章主要介紹了Golang?HTTP服務(wù)超時(shí)控制實(shí)現(xiàn)原理,HTTP服務(wù)的超時(shí)控制是保障服務(wù)高可用性的重要措施之一,由于HTTP服務(wù)可能會(huì)遇到網(wǎng)絡(luò)延遲,資源瓶頸等問題,因此需要對(duì)請(qǐng)求進(jìn)行超時(shí)控制,以避免服務(wù)雪崩等問題,需要的朋友可以參考下2023-05-05