在 Go 語言使用 Viper 管理設定檔

viper 在每個語言內一定都會有管理設定檔的相關套件,像是在 Node.jsdotenv 套件,而在 Go 語言內呢?相信大家一定都會推 Hugo 作者寫的 Viper,Viper 可以支援讀取 JSON, TOML, YAML, HCL 等格式的設定檔案,也可以讀取環境變數,另外也可以直接跟取遠端設定檔整合(像是 etcdConsul),本篇會介紹如何使用 Viper。

情境需求

當專案是使用 Yaml 或 JSON 存放設定檔時,在不同的部署環境都需要不同的設定檔。這時候就需要設定 App 可以指定不同設定檔路徑,指令如下
$ app -c config.yaml
這樣測試同事拿到執行檔時,就可以透過 -c 參數來讀取個人設定檔。有個問題,假設設定檔需要動態修改,每次測完就改動一次有點麻煩,所以 App 必須要支援環境變數,像是如下:
$ APP_PORT=8088 app -c config.yaml
假如沒有帶入 -c 參數,App 要能讀取系統預設環境設定檔案,像是 ($HOME/.app/config.yaml)。下面來教大家如何透過 Viper 做到上述環境。

建立預設檔案

在 Go 語言內可以先用變數方式將 Yaml 直接寫在程式碼內:
var defaultConf = []byte(`
app:
  port: 3000
`)
接著設定 Viper 讀取 Yaml 檔案型態。
viper.SetConfigType("yaml")

讀取指定檔案

透過 Go 語言的 flag 套件可以輕易實作出命令列 -c 參數
flag.StringVar(&configFile, "c", "", "Configuration file path.")
接著就可以直接讀取 Yaml 檔案
if configFile != "" {
    content, err := ioutil.ReadFile(confPath)

    if err != nil {
        return conf, err
    }

    viper.ReadConfig(bytes.NewBuffer(content))
}
可以看到透過 viper.ReadConfig 可以把 Yaml 內容丟進去,之後就可以透過 viper.GetInt("app.port") 來存取資料。

讀取動態目錄

Viper 有個功能就是可以直接幫忙找尋相關目錄內的設定檔案。先假設底下路徑是您希望 App 可以自動幫你讀取:
  1. /etc/app/ (Linux 常用的 /etc/ 目錄)
  2. $HOME/.app (家目錄底下的 .app 目錄)
  3. . (執行當下目錄)
首先設定 Viper 要去找 config 開頭的設定檔案
viper.SetConfigName("config")
上面設定好,就會直接找 config.yaml 檔案,如果設定 app 則是找 app.yaml。接著指定設定檔所在目錄
viper.AddConfigPath("/etc/app/")
viper.AddConfigPath("$HOME/.app")
viper.AddConfigPath(".")
最後透過 ReadInConfig 來自動搜尋並且讀取檔案。
if err := viper.ReadInConfig(); err == nil {
    fmt.Println("Using config file:", viper.ConfigFileUsed())
}

從環境變數讀取

如果專案需要跑在容器環境,這樣此功能對部署來說非常重要,也就是我只需要將 Go 語言的執行檔包進去 Docker 就好,而不需要將 Yaml 設定檔一起包入,或是透過 Volume 方式掛起來。這樣至少減少了一個步驟。首先設定 Viper 自動讀取環境變數:
// read in environment variables that match
viper.AutomaticEnv()
接著設定環境變數 Prefix,避免跟其他專案衝突
// will be uppercased automatically
viper.SetEnvPrefix("test")
最後設定環境變數的分隔符號從 . 換成 _
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
以上面的例子來說,你可以透過 TEST_APP_PORT 來指定不同的 port
$ TEST_APP_PORT=3001 app -c config.yaml

實作範例

我們可以把上面的說明整理成範例,讀取流程會如下
  1. 讀取實體路徑
  2. 讀取預設路徑
  3. 讀取預設設定
假如 App 讀取特定路徑設定檔 (-c 參數),那就不會執行 2, 3 步驟,步驟 1 省略的話,App 就會自動先找預設路徑,如果預設路徑找不到就會執行步驟 3。程式碼範例如下:
viper.SetConfigType("yaml")
viper.AutomaticEnv()         // read in environment variables that match
viper.SetEnvPrefix("gorush") // will be uppercased automatically
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))

if confPath != "" {
    content, err := ioutil.ReadFile(confPath)

    if err != nil {
        return conf, err
    }

    viper.ReadConfig(bytes.NewBuffer(content))
} else {
    // Search config in home directory with name ".gorush" (without extension).
    viper.AddConfigPath("/etc/gorush/")
    viper.AddConfigPath("$HOME/.gorush")
    viper.AddConfigPath(".")
    viper.SetConfigName("config")

    // If a config file is found, read it in.
    if err := viper.ReadInConfig(); err == nil {
        fmt.Println("Using config file:", viper.ConfigFileUsed())
    } else {
        // load default config
        viper.ReadConfig(bytes.NewBuffer(defaultConf))
    }
}

結論

我個人用 Viper 最大的原因就是可以透過環境變數修改 App 的預設參數,另外編譯 Docker 容器時也不需要將設定檔丟入。在 Kubernetes 架構內可以透過 config map 方式來動態改變 App 行為。如果要搭配命令列,可以使用 cobra 結合 Viper。