前言
本文主要是為讀者介紹一個輕便好用的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: false
Favorite:
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的功能,這些大家感興趣可以自己去了解一下。
有趣的應用
雖然Unmarshal Struct已經足夠好用了,但有作者還是想開發一下新的玩法,比如說這個配置文件和當前的新版本不是很匹配,當然實際生產中我們是要講究向下兼容的。
var yamlConfig = YamlConfig{};
ycType := reflect.TypeOf(yamlConfig);
for i := 0 ; i < ycType.NumField();i++{
name := ycType.Field(i).Name;
element := reflect.ValueOf(yamlConfig).Field(i).Interface();
if err = config.UnmarshalKey(name, element); err != nil{
logger.Errorf("Error reading configuration:", err);
}
}
如上代碼所示,我們從最外圍的結構體中找出子元素的名稱和interface,然后分別解析,這樣及時某一項缺失了我們也可以及時提醒用戶,或者設置缺省配置,還有很多好玩的方法,大家可以互相參考哦。