在上次的實踐中, 啓動了一個基礎的 restful api server.git
當時的代碼中有不少硬編碼的屬性, 此次就要嘗試從配置文件中讀取.github
這裏使用 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")
或者這個環境變量對應的值了.工具
使用 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
定義命令的名字, Short
和 Long
分別是短長描述, 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