首頁 > 軟體

Go中的應用設定管理詳解

2022-09-02 18:02:28

問題

  • Go語言在編譯時不會將組態檔這類第三方檔案打包進二進位制檔案中
  • 它既受當前路徑的影響,也會因所填寫的不同而改變,並非是絕對可靠的

解決

命令列引數

在Go語言中,可以直接通過flag標準庫來實現該功能。實現邏輯為,如果存在命令列引數,則優先使用命令列引數,否則使用組態檔中的設定引數。

如下:

var (
	port    string
	runMode string
	config  string
)
func init() {	
	// 獲取命令列引數
	err = setupFlag()
	if err != nil {
		log.Fatalf("init.setupFlag err: %v", err)
	}
    ......
}
// 獲取命令列引數
func setupFlag() error {
	flag.StringVar(&port, "port", "", "啟動埠")
	flag.StringVar(&runMode, "mode", "", "啟動模式")
	flag.StringVar(&config, "config", "config/", "指定要使用的組態檔路徑")
	flag.Parse()
	return nil
}

通過上述程式碼,我們可以通過標準庫flag讀取命令列引數,然後根據其預設值判斷組態檔是否存在。若存在,則對讀取設定的路徑進行變更,程式碼如下:

package setting
import "github.com/spf13/viper"
type Setting struct {
	vp *viper.Viper
}
// 初始化組態檔的基礎屬性
func NewSetting(configs ...string) (*Setting, error) {
	vp := viper.New()
	vp.SetConfigName("config")
	if len(configs) != 0 {
		for _, config := range configs {
			if config != "" {
				vp.AddConfigPath(config)
			}
		}
	} else {
		vp.AddConfigPath("configs/")
	}
	vp.SetConfigType("yaml")
	err := vp.ReadInConfig()
	if err != nil {
		return nil, err
	}
	return &Setting{vp}, nil
}

接下來,對ServerSetting設定項進行覆寫。如果存在,則覆蓋原有的檔案設定,使其優先順序更高,程式碼如下:

// 初始化組態檔
func setupSetting() error {
	setting, err := setting2.NewSetting(strings.Split(config, ",")...)
	if err != nil {
		return err
	}
	......
	if port != "" {
		global.ServerSetting.HttpPort = port
	}
	if runMode != "" {
		global.ServerSetting.RunMode = runMode
	}
	return nil
}

然後在執行的時候傳入引數即可:

go run main.go -port=8081 -mode=debug -config=configs/

系統環境變數

可以將組態檔存放在系統自帶的全域性變數中,如$HOME/conf或/etc/conf中,這樣做的好處是不需要重新自定義一個新的系統環境變數。

一般來說,我們會在程式中內建一些系統環境變數的讀取,其優先順序低於命令列引數,但高於檔案設定。

打包進二進位制檔案

可以將組態檔這種第三方檔案打包進二進位制檔案中,這樣就不需要過度關注這些第三方檔案了。但這樣做是有一定代價的,因此要注意使用的應用場景,即並非所有的專案都能這樣操作。

首先安裝go-bindata庫,安裝命令如下:

go get -u github.com/go-bindata/go-bindata/...

通過go-bindata庫可以將資料檔案轉換為Go程式碼。例如,常見的組態檔、資原始檔(如Swagger UI)等都可以打包進Go程式碼中,這樣就可以“擺脫”靜態資原始檔了。接下來在專案根目錄下執行生成命令:

go-bindata -o configs/config.go -pkg-configs configs/config.yaml

執行這條命令後,會將 configs/config.yaml 檔案打包,並通過-o 選項指定的路徑輸出到configs/config.go檔案中,再通過設定的-pkg選項指定生成的packagename為configs,接下來只需執行下述程式碼,就可以讀取對應的檔案內容了:

b,_:=configs.Asset("configs/config.yaml")

把第三方檔案打包進二進位制檔案後,二進位制檔案必然增大,而且在常規方法下無法做檔案的熱更新和監聽,必須要重啟並且重新打包才能使用最新的內容,因此這種方式是有利有弊的。

設定熱更新

開源的fsnotify

既然要做設定熱更新,那麼首先要知道設定是什麼時候修改的,做了哪些事?因此我們需要對所設定的檔案進行監聽,只有監聽到了,才能知道它做了哪些變更。

開源庫 fsnotify 是用Go語言編寫的跨平臺檔案系統監聽事件庫,常用於檔案監聽,因此我們可以藉助該庫來實現這個功能。

(1)安裝

go get -u golang.org/x/sys/...
go get -u github.com/fsnotify/fsnotify

fsnotify是基於golang.org/x/sys實現的,並非syscall標準庫,因此在安裝的同時需要更新其版本,確保版本是最新的。

(2)案例

package main
import (
	"gopkg.in/fsnotify.v1"
	"log"
)
func main() {
	watcher, _ := fsnotify.NewWatcher()
	defer watcher.Close()
	done := make(chan bool)
	go func() {
		for {
			select {
			case event, ok := <-watcher.Events:
				if !ok {
					return
				}
				log.Fatal("event: ", event)
				if event.Op&fsnotify.Write == fsnotify.Write {
					log.Println("modified file:", event.Name)
				}
			case err, ok := <-watcher.Errors:
				if !ok {
					return
				}
				log.Fatal("error:", err)
			}
		}
	}()
	path := "configs/config.yaml"
	_ = watcher.Add(path)
	<-done
}

通過監聽,我們可以很便捷地知道檔案做了哪些變更。進一步來說,我們可以通過對其進行二次封裝,在它的上層實現一些變更動作來完成組態檔的熱更新。

使用viper開源庫實現熱更新

viper開源庫能夠很便捷地實現對檔案的監聽和熱更新。

開啟pkg/setting/section.go檔案,針對過載應用設定項,新增如下處理方法:

var sections = make(map[string]interface{})
// 解析組態檔
func (s *Setting) ReadSection(k string, v interface{}) error {
	err := s.vp.UnmarshalKey(k, v)
	if err != nil {
		return err
	}
	if _,ok:=sections[k];!ok{
		sections[k] = v
	}
	return nil
}

首先修改ReadSection方法,增加讀取section的儲存記錄,以便在重新載入設定的方法中進行二次處理。接下來新增ReloadAllSection方法,重新讀取設定,程式碼如下:

// 讀取所有設定
func (s *Setting) ReadAllSections() error {
	for k, v := range sections {
		err := s.ReadSection(k, v)
		if err != nil {
			return err
		}
	}
	return nil
}
// 監聽設定變化
func (s *Setting) WatchSettingChange()  {
	go func() {
		s.vp.WatchConfig()
		s.vp.OnConfigChange(func(in fsnotify.Event) {
			_ = s.ReloadAllSections()
		})
	}()
}

最後在pkg/setting/setting.go檔案中新增檔案熱更新的監聽和變更處理,程式碼如下:

// 初始化組態檔的基礎屬性
func NewSetting(configs ...string) (*Setting, error) {
	vp := viper.New()
	vp.SetConfigName("config")
	for _, config := range configs {
		if config != "" {
			vp.AddConfigPath(config)
		}
	}
	vp.SetConfigType("yaml")
	err := vp.ReadInConfig()
	if err != nil {
		return nil, err
	}
    // 加入熱更新
	s := &Setting{vp: vp}
	s.WatchSettingChange()
	return s, nil
}

在上述程式碼中,首先在NewSetting方法中起一個協程,再在裡面通過WatchConfig方法對檔案設定進行監聽,並在OnConfigChange方法中呼叫剛剛編寫的過載方法ReloadAllSection來處理熱更新的檔案監聽事件回撥,這樣就可以“悄無聲息”地實現一個檔案設定熱更新了。

OnConfigChange方法的回撥方法形參,其實就是fsnotify。

以上就是Go中的應用設定管理詳解的詳細內容,更多關於Go應用設定管理的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com