使用 Go 讀取配置文件

簡介

在上次的實踐中, 啓動了一個基礎的 restful api server.git

當時的代碼中有不少硬編碼的屬性, 此次就要嘗試從配置文件中讀取.github

使用 viper 讀取配置

這裏使用 viper 讀取配置, 首先安裝一下.web

go get -u github.com/spf13/viper
複製代碼

建立一個 config 目錄, 而後添加 config.go 文件, 在裏面定義一個結構 Config, 使用 Name 保存配置路徑.api

type Config struct {
	Name string
}
複製代碼

而後定義它的兩個方法, 一個讀取配置, 另外一個觀察配置的改動.bash

// 讀取配置
func (c *Config) InitConfig() error {
	if c.Name != "" {
		viper.SetConfigFile(c.Name)
	} else {
		viper.AddConfigPath("conf")
		viper.SetConfigName("config")
	}
	viper.SetConfigType("yaml")

	// 從環境變量總讀取
	viper.AutomaticEnv()
	viper.SetEnvPrefix("web")
	viper.SetEnvKeyReplacer(strings.NewReplacer("_", "."))

	return viper.ReadInConfig()
}

// 監控配置改動
func (c *Config) WatchConfig(change chan int) {
	viper.WatchConfig()
	viper.OnConfigChange(func(e fsnotify.Event) {
		log.Printf("配置已經被改變: %s", e.Name)
		change <- 1
	})
}
複製代碼

讀取配置時定義了多種方式, 第一個種是沒有定義 Config.Name, c.Name 爲空字符串的狀況, 這時會從默認路徑中尋找配置文件.服務器

另一種就是直接指定了配置文件的路徑, 那是就直接使用這個配置文件.restful

另外, 激活了從環境變量中讀取配置參數, 注意設置了全部環境變量的前綴, 前綴會自動轉換爲 大寫_ 的格式.app

另外, 對於多層級的配置參數來講, 會自動將環境變量中的 _ 轉換爲 ..函數

舉個例子, 當前設置的前綴爲 web. 定義一個環境變量名爲 WEB_LOG_PATH, 會自動轉換爲 log.path, 就能夠使用 viper.GetString("log.path") 或者這個環境變量對應的值了.工具

使用 Cobra 建立命令行工具

使用 viper 讀取配置以後, 爲了更靈活的使用, 勢必要使用 CLI 工具, 以便在運行時能夠指定參數等.

Cobra 是一個用於建立現代化的 CLI 界面的庫, 能提供相似於 git 和 go 工具的能力.

Cobra 的做者就是建立 viper 的做者, 因此這些庫都是以 🐍 命名的, viper 是蝰蛇, corba 是眼鏡蛇.

corba 擅長於聚合多個命令, 它遵循 命令, 參數, 標誌 的理念.

聽從這種理念的模式是 APPNAME VERB NOUN --ADJECTIVE 或者 APPNAME COMMAND ARG --FLAG.

對於咱們的 web 項目來講, 目前只有啓動這個操做, 因此咱們先建立一個主動做.

建立 cmd 目錄, 並建立一個名爲 root.go 的文件.

var rootCmd = &cobra.Command{
	Use:   "server",
	Short: "server is a simple restful api server",
	Long: `server is a simple restful api server use help get more ifo`,
	Run: func(cmd *cobra.Command, args []string) {
		runServer()
	},
}
複製代碼

主要是使用 &cobra.Command 定義一個命令.

裏面的參數 Use 定義命令的名字, ShortLong 分別是短長描述, Run 定義了實際要運行的代碼.

定義好主命令以後, 可能須要添加一些操做, 這些都是定義在 init() 函數中的, 同時在裏面運行了 cobra.OnInitialize, 這會在每一個命令的執行階段被運行.

// 初始化, 設置 flag 等
func init() {
	cobra.OnInitialize(initConfig)
	rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default: ./conf/config.yaml)")
}

// 初始化配置
func initConfig() {
	c := config.Config{
		Name: cfgFile,
	}

	if err := c.InitConfig(); err != nil {
		panic(err)
	}
	log.Printf("載入配置成功")
	c.WatchConfig(configChange)
}
複製代碼

我在這裏設置了一個名爲 config 的 flag, 即配置文件對應的路徑.

最後, 還須要定義一個函數, 用來包裝主命令的執行:

// 包裝了 rootCmd.Execute()
func Execute() {
	if err := rootCmd.Execute(); err != nil {
		log.Println(err)
		os.Exit(1)
	}
}
複製代碼

如此一來, 主文件 main.go 就很是簡單了, 由於咱們已經把主要的執行操做, 封裝爲 runServer(), 並定義在主命令之下了.

func main() {
	cmd.Execute()
}
複製代碼

熱重載

前面定義了一個觀察 viper 配置改變的函數, 注意到它有個通道參數, 我使用通道做爲消息傳遞機制.

// 監控配置改動
func (c *Config) WatchConfig(change chan int) {
	viper.WatchConfig()
	viper.OnConfigChange(func(e fsnotify.Event) {
		log.Printf("配置已經被改變: %s", e.Name)
		change <- 1
	})
}
複製代碼

當配置文件被改變以後, 其實它自己會傳遞一個叫作 fsnotify.Event, 但我沒有仔細研究, 而是採用了通道傳遞消息.

// 定義 rootCmd 命令的執行
func runServer() {
	// 設置運行模式
	gin.SetMode(viper.GetString("runmode"))

	// 初始化空的服務器
	app := gin.New()
	// 保存中間件
	middlewares := []gin.HandlerFunc{}

	// 路由
	router.Load(
		app,
		middlewares...,
	)

	go func() {
		if err := check.PingServer(); err != nil {
			log.Fatal("服務器沒有響應", err)
		}
		log.Printf("服務器正常啓動")
	}()

	// 服務器裕興的地址和端口
	addr := viper.GetString("addr")
	log.Printf("啓動服務器在 http address: %s", addr)

	srv := &http.Server{
		Addr:    addr,
		Handler: app,
	}
	// 啓動服務
	go func() {
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("listen: %s\n", err)
		}
	}()

	// 等待配置改變, 而後重啓
	<-configChange
	if err := srv.Shutdown(context.Background()); err != nil {
		log.Fatal("Server Shutdown:", err)
	}
	runServer()
}
複製代碼

前面都是些常規的運行啓動, 包括使用一個 goroutine 檢查啓動的健康狀態, 使用另外一個 goroutine 啓動服務器.

注意最後幾行, 咱們在等待通道通知配置文件已經發生了改變, 而後開始先關閉服務器, 最後從新運行啓動函數.

注意: 這裏可能有個 bug, 那就是修改配置文件後, OnConfigChange 會觸發兩次, 暫時沒有什麼好的解決方法. 或者能夠考慮一下 github issues 上提到的 限流模式.

總結

這個過程主要研究瞭如何讀取配置文件, 同時也使用了命令行相關的庫, 便於之後擴展更多的命令.

當前部分的代碼

做爲版本 0.2.0

相關文章
相關標籤/搜索