前言
本文主要是為讀者介紹一個輕便好用的Golang配置庫viper
本文
viper 的功能
viper 支援以下功能:
1. 支援Yaml、Json、 TOML、HCL 等格式的配置
2. 可以從檔案、io、環境變數、command line中提取配置
3. 支援自動轉換的類型解析
4. 可以遠程從etcd中讀取配置
範例程式碼
定義一個類型:
type config struct { v *viper.Viper;}
用於測試的Yaml設定檔 config.yaml
TimeStamp: "2018-07-16 10:23:19"Author: "WZP"PassWd: "Hello"Information: Name: "Harry" Age: "37" Alise: - "Lion" - "NK" - "KaQS" Image: "/path/header.rpg" Public: falseFavorite: Sport: - "swimming" - "football" Music: - "zui xuan min zu feng" LuckyNumber: 99
讀取yaml設定檔
func LoadConfigFromYaml (c *config) error { c.v = viper.New(); //設定設定檔的名字 c.v.SetConfigName("config") //添加設定檔所在的路徑,注意在Linux環境下%GOPATH要替換為$GOPATH c.v.AddConfigPath("%GOPATH/src/") c.v.AddConfigPath("./") //設定設定檔類型 c.v.SetConfigType("yaml"); if err := c.v.ReadInConfig(); err != nil{ return err; } log.Printf("age: %s, name: %s \n", c.v.Get("information.age"), c.v.Get("information.name")); return nil;}
注意:如果不用AddConfigPath去指定路徑,它會在程式執行的目錄去尋找config.yaml
從IO中讀取配置
//由IO讀取配置func ReadConfigFormIo(c *config) error { c.v = viper.New() if f, err := os.Open("config.yaml"); err != nil{ log.Printf("filure: %s", err.Error()); return err; }else { confLength, _ :=f.Seek(0,2); //注意,通常寫c++的習慣害怕讀取字串的時候越界,都會多留出一個NULL在末尾,但是在這裡不行,會報出如下錯誤: //While parsing config: yaml: control characters are not allowed //錯誤參考網址:https://stackoverflow.com/questions/33717799/go-yaml-control-characters-are-not-allowed-error configData := make([]byte, confLength); f.Seek(0, 0); f.Read(configData); log.Printf("%s\n", string(configData)) c.v.SetConfigType("yaml"); if err := c.v.ReadConfig(bytes.NewBuffer(configData)); err != nil{ log.Fatalf(err.Error()); } } log.Printf("age: %s, name: %s \n", c.v.Get("information.age"), c.v.Get("information.name")); return nil;}
上面的代碼是把設定檔中的資料匯入IO,然後再從IO中讀取
從環境變數中讀取配置
//讀取本地的環境變數func EnvConfigPrefix(c *config) error { c.v = viper.New(); //BindEnv($1,$2) // 如果只傳入一個參數,則會提取指定的環境變數$1,如果設定了首碼,則會自動補全 首碼_$1 //如果傳入兩個參數則不會補全首碼,直接擷取第二參數中傳入的環境變數$2 os.Setenv("LOG_LEVEL", "INFO"); if nil == c.v.Get("LOG_LEVEL ") { log.Printf("LOG_LEVEL is nil"); }else { return ErrorNotMacth; } //必須要綁定後才能擷取 c.v.BindEnv("LOG_LEVEL"); log.Printf("LOG_LEVEL is %s", os.Getenv("log_level")); //會擷取所有的環境變數,同時如果過設定了首碼則會自動補全首碼名 c.v.AutomaticEnv(); //環境變數首碼大小寫不區分 os.Setenv("DEV_ADDONES","none"); log.Printf("DEV_ADDONES: %s", c.v.Get("dev_addones")); //SetEnvPrefix會設定一個環境變數的首碼名 c.v.SetEnvPrefix("DEV"); os.Setenv("DEV_MODE", "true"); //此時會自動補全首碼,實際去擷取的是DEV_DEV_MODE if nil == c.v.Get("dev_mode"){ log.Printf("DEV_MODE is nil") ; }else { return ErrorNotMacth; } //此時我們直接指定了loglevel所對應的環境變數,則不會去補全首碼 c.v.BindEnv("loglevel", "LOG_LEVEL"); log.Printf("LOG_LEVEL: %s", c.v.Get("loglevel")) ; return nil}
SetEnvPrefix 和 AutomaticEnv、BindEnv搭配使用很方便,比如說我們把當前程式的環境變數都設定為xx_ ,這樣方便我們管理,也避免和其他環境變數衝突,而在讀取的時候又很方便的就可以讀取。
方便的替換符
func EnvCongiReplacer(c *config, setPerfix bool) error { c.v = viper.New(); c.v.AutomaticEnv(); c.v.SetEnvKeyReplacer(strings.NewReplacer(".","_")); os.Setenv("API_VERSION","v0.1.0"); //Replacer和prefix一起使用可能會衝突,比如我下面的例子 //因為會自動補全首碼最終由擷取API_VERSION變成API_API_VERSION if setPerfix{ c.v.SetEnvPrefix("api");} if s := c.v.Get("api.version"); s==nil{ return ErrorNoxExistKey }else { log.Printf("%s", c.v.Get("api.version")); } return nil;}
我們有時候需要去替換key中的某些字元,來轉化為對應的環境變臉,比如說例子中將' . '替換為'_' ,由擷取api.version變成了api_version,但是有一點需要注意的,SetEnvPrefix和SetEnvKeyReplacer一起用的時候可能會混淆。
別名功能
//設定重載 和別名func SetAndAliases(c *config) error { c.v = viper.New(); c.v.Set("Name","wzp"); c.v.RegisterAlias("id","Name"); c.v.Set("id","Mr.Wang"); //我們可以發現當別名對應的值修改之後,原本的key也發生變化 log.Printf("id %s, name %s",c.v.Get("id"),c.v.Get("name") ); return nil;}
我們可以為key設定別名,當別名的值被重設後,原key對應的值也會發生變化。
'
序列化和還原序列化
type favorite struct { Sports []string; Music []string; LuckyNumber int;}type information struct { Name string; Age int; Alise []string; Image string; Public bool}type YamlConfig struct { TimeStamp string Author string PassWd string Information information Favorite favorite;}//將配置解析為Struct對象func UmshalStruct(c *config) error { LoadConfigFromYaml(c); var cf YamlConfig if err := c.v.Unmarshal(&cf); err != nil{ return err; } return nil;}func YamlStringSettings(c *config) string { c.v = viper.New(); c.v.Set("name", "wzp"); c.v.Set("age", 18); c.v.Set("aliase",[]string{"one","two","three"}) cf := c.v.AllSettings() bs, err := yaml.Marshal(cf) if err != nil { log.Fatalf("unable to marshal config to YAML: %v", err) } return string(bs)}func JsonStringSettings(c *config) string { c.v = viper.New(); c.v.Set("name", "wzp"); c.v.Set("age", 18); c.v.Set("aliase",[]string{"one","two","three"}) cf := c.v.AllSettings() bs, err := json.Marshal(cf) if err != nil { log.Fatalf("unable to marshal config to YAML: %v", err) } return string(bs)}
超級實惠的一個功能,直接把配置還原序列化到一個結構體,爽歪歪有木有?也可以把設定直接序列化為我們想要的類型:yaml、json等等
從command Line中讀取配置
func main() { flag.String("mode","RUN","please input the mode: RUN or DEBUG"); pflag.Int("port",1080,"please input the listen port"); pflag.String("ip","127.0.0.1","please input the bind ip"); //擷取標準包的flag pflag.CommandLine.AddGoFlagSet(flag.CommandLine); pflag.Parse(); //BindFlag //在pflag.Init key後面使用 viper.BindPFlag("port", pflag.Lookup("port")); log.Printf("set port: %d", viper.GetInt("port")); viper.BindPFlags(pflag.CommandLine); log.Printf("set ip: %s", viper.GetString("ip"));}
可以使用標準的flag也可以使用viper包中內建的pflag,作者建議使用pflag。
監聽設定檔
//監聽設定檔的修改和變動func WatchConfig(c *config) error { if err := LoadConfigFromYaml(c); err !=nil{ return err; } ctx, cancel := context.WithCancel(context.Background()); c.v.WatchConfig() //監聽回呼函數 watch := func(e fsnotify.Event) { log.Printf("Config file is changed: %s \n", e.String()) cancel(); } c.v.OnConfigChange(watch); <-ctx.Done(); return nil;}
重點來了啊,這個可以說是非常非常實用的一個功能,以往我們修改設定檔要麼重啟服務,要麼搞一個api去修改,Viper把這個功能幫我們實現了。只要設定檔被修改儲存後,我們事先註冊的watch函數就回被觸發,只要我們在這裡面添加更新操作就ok了。不過美中不足的是,它目前只監聽設定檔。
拷貝子分支
func TestSubConfig(t *testing.T) { c := config{}; LoadConfigFromYaml(&c); sc := c.v.Sub("information"); sc.Set("age", 80); scs,_:=yaml.Marshal(sc.AllSettings()) t.Log(string(scs)); t.Logf("age: %d", c.v.GetInt("information.age"));}
拷貝一個子分支最大的用途就是我們可以複製一份配置,這樣在修改拷貝的時候原配置不會被修改,如果修改的配置出現了問題,我們可以方便的復原。
擷取配置項的方法
//測試各種get類型func TestGetValues(t *testing.T) { c := &config{} if err := LoadConfigFromYaml(c); err != nil{ t.Fatalf("%s: %s",t.Name(), err.Error()); } if info := c.v.GetStringMap("information"); info != nil{ t.Logf("%T", info); } if aliases := c.v.GetStringSlice("information.aliases"); aliases != nil{ for _, a := range aliases{ t.Logf("%s",a); } } timeStamp := c.v.GetTime("timestamp"); t.Logf("%s", timeStamp.String()); if public := c.v.GetBool("information.public"); public{ t.Logf("the information is public"); } age := c.v.GetInt("information.age"); t.Logf("%s age is %d", c.v.GetString("information.name"), age);}
如果我們直接用Get擷取的傳回值都是interface{}類型,這樣我們還要手動轉化一下,可以直接指定類型去擷取,方便快捷。
除了以上所說的功能外,viper還有從etcd提取配置以及自訂flage的功能,這些大家感興趣可以自己去瞭解一下。