Go 項目開發中如何讀取應用配置?

幾乎所有的後端服務都需要一些配置項來配置我們的服務,一些小型的項目,配置不是很多,可以選擇只通過命令行參數來傳遞配置,但是大型項目配置很多,通過命令行參數傳遞就變的很麻煩,不好維護,標準的解決方案是將這些配置信息保存在配置文件中,由程序啓動時加載和解析。所以對於一個稍微大型點的系統,配置文件加載和解析就是一個剛需。Go 生態中有很多包可以加載並解析配置,目前最受歡迎的是 Viper 包。

Viper 包介紹

Viper 是 Go 應用程序現代化、完整的解決方案,能夠處理不同格式的配置文件,讓我們在構建現代應用程序時,不必擔心配置文件格式。Viper 也能夠滿足我們對應用配置的各種需求。Viper 有 很多特性,其中一些比較重要的特性如下:

Viper 可以從不同的位置讀取配置,不同位置的配置具有不同的優先級,高優先的配置會覆蓋低優先級相同的配置,按優先級從高到低排列如下:

  1. 通過 viper.Set 函數顯式設置的配置;

  2. 命令行參數;

  3. 環境變量;

  4. 配置文件;

  5. key/value 存儲;

  6. 默認值。

Viper 因爲其強大的特性,越來越多的優秀項目開始使用 Viper 作爲其配置解析工具,例如:Hugo、Docker Notary、Clairctl、Mercure 等。

這裏需要注意,Viper 配置鍵不區分大小寫。

Viper 使用方法

Viper 有很多功能,這裏選擇一些常用的、重要的功能來講解。在使用 viper 的過程中,最重要的 2 類功能就是讀入配置和讀取配置,Viper 提供不同的方式來讀入配置和讀取配置。

讀入配置,將配置讀入到 viper 中,有如下讀入方式:

讀入配置

  1. 設置默認值

一個好的配置系統應該支持默認值。viper 支持對 key 設置默認值,當沒有通過配置文件,環境變量,遠程配置或命令行標誌設置 key 時,設置默認值通常是很有用的,可以使程序即使在沒有明確指定配置時,也能夠正常運行。例如:

viper.SetDefault("ContentDir""content")
viper.SetDefault("LayoutDir""layouts")
viper.SetDefault("Taxonomies", map[string]string{"tag""tags""category""categories"})
  1. 讀取配置文件

viper 可以讀取配置文件來解析配置,支持 jsontomlyamlymlpropertiespropsprophcldotenvenv 格式的配置文件。Viper 可以搜索多個路徑,但目前單個 Viper 實例僅支持單個配置文件。Viper 不默認任何配置搜索路徑,將默認決策留給應用程序。

以下是如何使用 Viper 搜索和讀取配置文件的示例:

package main

import (
 "fmt"

 "github.com/spf13/pflag"
 "github.com/spf13/viper"
)

var (
 cfg  = pflag.StringP("config""c""""Configuration file.")
 help = pflag.BoolP("help""h", false, "Show this help message.")
)

func main() {
 pflag.Parse()
 if *help {
  pflag.Usage()
  return
 }

  // 從配置文件中讀取配置
 if *cfg != "" {
  viper.SetConfigFile(*cfg)   // 指定配置文件名
  viper.SetConfigType("yaml") // 如果配置文件名中沒有文件擴展名,則需要指定配置文件的格式,告訴 viper 以何種格式解析文件
 } else {
  viper.AddConfigPath(".")          // 把當前目錄加入到配置文件的搜索路徑中
  viper.AddConfigPath("$HOME/.iam") // 配置文件搜索路徑,可以設置多個配置文件搜索路徑
  viper.SetConfigName("config")     // 配置文件名稱(沒有文件擴展名)
 }

 if err := viper.ReadInConfig(); err != nil { // 讀取配置文件。如果指定了配置文件名,則使用指定的配置文件,否則在註冊的搜索路徑中搜索
  panic(fmt.Errorf("Fatal error config file: %s \n", err))
 }

 fmt.Printf("Used configuration file is: %s\n", viper.ConfigFileUsed())
}

在加載配置文件出錯時,你可以像下面這樣處理找不到配置文件的特定情況:

if err := viper.ReadInConfig(); err != nil {
    if _, ok := err.(viper.ConfigFileNotFoundError); ok {
        // 配置文件未找到錯誤;如果需要可以忽略
    } else {
        // 配置文件被找到,但產生了另外的錯誤
    }
}

// 配置文件找到併成功解析

viper 支持設置多個配置文件搜索路徑,需要注意添加搜索路徑的順序,viper 會根據添加的路徑順序搜索配置文件,如果找到則停止搜索。如果調用 SetConfigFile 直接指定了配置文件名,並且配置文件名沒有文件擴展名時,需要顯試指定配置文件的格式,以使 viper 能夠正確解析配置文件。

如果通過搜索的方式查找配置文件,則需要注意 SetConfigName 設置的配置文件名是不帶擴展名的,在搜索時 viper 會在文件名之後追加文件擴展名,並嘗試搜索所有支持的擴展類型。比如,如果我們通過 SetConfigName 設置了配置文件名爲 config,則 viper 會在註冊的搜索路徑中,依次搜索:config.jsonconfig.tomlconfig.yamlconfig.ymlconfig.propertiesconfig.propsconfig.propconfig.hclconfig.dotenvconfig.env

  1. 寫入配置文件

讀取配置文件很有用,但有時候我們可能需要將程序中當前的配置保存起來,方便後續使用或者 debug,viper 提供了一系列的函數,可以讓我們把當前的配置保存到文件中,viper 提供瞭如下函數來保存配置:

根據經驗,標記爲 Safe 的所有方法都不會覆蓋任何文件,而是直接創建(如果不存在),而默認行爲是創建或截斷。

一個小示例:

viper.WriteConfig()
viper.SafeWriteConfig()
viper.WriteConfigAs("config.running.yaml")
viper.SafeWriteConfigAs("config.running.yaml")
  1. 監聽和重新讀取配置文件

Viper 支持在運行時讓應用程序實時讀取配置文件,也就是熱加載配置。可以通過 WatchConfig 函數熱加載配置。在調用 WatchConfig 函數之前,請確保已經添加了配置文件的搜索路徑。可選地,可以爲 Viper 提供一個回調函數,以便在每次發生更改時運行。

示例:

viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
   // 配置文件發生變更之後會調用的回調函數
 fmt.Println("Config file changed:", e.Name)
})
  1. 設置配置值

我們可以通過 viper.Set() 函數來顯式設置配置:

viper.Set("user.username""colin")
  1. 註冊和使用別名

別名允許多個鍵引用單個值。示例:

viper.RegisterAlias("loud""Verbose")
viper.Set("verbose"true) // 效果等同於下面一行代碼
viper.Set("loud"true)   // 效果等同於上面一行代碼

viper.GetBool("loud") // true
viper.GetBool("verbose") // true
  1. 使用環境變量

viper 還支持環境變量,通過如下 5 個函數來支持環境變量:

這裏要注意:viper 讀取環境變量是區分大小寫的。viper 提供了一種機制來確保 ENV 變量是唯一的。通過使用 SetEnvPrefix,可以告訴 Viper 在讀取環境變量時使用前綴。BindEnvAutomaticEnv 都將使用此前綴。比如,我們設置了 viper.SetEnvPrefix("VIPER"),當使用 viper.Get("apiversion") 時,實際讀取的環境變量是 VIPER_APIVERSION

BindEnv 需要一個或兩個參數。第一個參數是鍵名,第二個是環境變量的名稱,環境變量的名稱區分大小寫。如果未提供 ENV 變量名,則 viper 將假定 ENV 變量名爲:環境變量前綴_鍵名全大寫 ,例如:前綴爲 VIPER,key 爲 username,則 ENV 變量名爲:VIPER_USERNAME 。當顯式提供 ENV 變量名(第二個參數)時,它不會自動添加前綴。例如,如果第二個參數是 id,Viper 將查找環境變量 ID

在使用 ENV 變量時,需要注意的一件重要事情是,每次訪問該值時都將讀取它。Viper 在調用 BindEnv 時不固定該值。

還有一個魔法函數 SetEnvKeyReplacerSetEnvKeyReplacer 允許你使用 strings.Replacer 對象來重寫 Env 鍵。如果你想在 Get() 調用中使用 - 或者 . ,但希望你的環境變量使用 _ 分隔符,可以通過 SetEnvKeyReplacer 來實現。比如,我們設置了環境變量 USER_SECRET_KEY=bVix2WBv0VPfrDrvlLWrhEdzjLpPCNYb,但我們想用 viper.Get("user.secret-key"),我們調用函數:

viper.SetEnvKeyReplacer(strings.NewReplacer(".""_""-""_"))

上面的代碼,在調用 viper.Get() 函數時,會用 _ 替換 .- 。默認情況下,空環境變量被認爲是未設置的,並將返回到下一個配置源。若要將空環境變量視爲已設置,可以使用 AllowEmptyEnv 方法。使用環境變量示例如下:

// 使用環境變量
os.Setenv("VIPER_USER_SECRET_ID""QLdywI2MrmDVjSSv6e95weNRvmteRjfKAuNV")
os.Setenv("VIPER_USER_SECRET_KEY""bVix2WBv0VPfrDrvlLWrhEdzjLpPCNYb")

viper.AutomaticEnv() // 讀取環境變量
viper.SetEnvPrefix("VIPER") // 設置環境變量前綴:VIPER_,如果是 viper,將自動轉變爲大寫。
viper.SetEnvKeyReplacer(strings.NewReplacer(".""_""-""_")) // 將 viper.Get(key) key 字符串中 '.' 和 '-' 替換爲 '_'
viper.BindEnv("user.secret-key")
viper.BindEnv("user.secret-id""USER_SECRET_ID") // 綁定環境變量名到 key
  1. 使用標誌

viper 支持 pflag 包,能夠綁定 key 到 flag。與 BindEnv 類似,在調用綁定方法時,不會設置該值。但在訪問它時會設置。對於單個標誌,可以調用 BindPFlag() 進行綁定:

viper.BindPFlag("token", pflag.Lookup("token")) // 綁定單個標誌

還可以綁定一組現有的 pflags(pflag.FlagSet):

viper.BindPFlags(pflag.CommandLine)             //綁定標誌集

讀取配置

viper 提供瞭如下方法來讀取配置:

每一個 Get 方法在找不到值的時候都會返回零值。爲了檢查給定的鍵是否存在,可以使用 IsSet() 方法。<Type> 可以是 viper 支持的類型首字母大寫:BoolFloat64IntIntSliceStringStringMapStringMapStringStringSliceTimeDuration。例如:GetInt()

讀取配置具體使用方法如下:

  1. 訪問嵌套的鍵

例如:加載下面的 JSON 文件:

{
    "host"{
        "address""localhost",
        "port"5799
    },
    "datastore"{
        "metric"{
            "host""127.0.0.1",
            "port"3099
        },
        "warehouse"{
            "host""198.0.0.1",
            "port"2112
        }
    }
}

Viper 可以通過傳入 . 分隔的路徑來訪問嵌套字段:

viper.GetString("datastore.metric.host") // (返回 "127.0.0.1")

如果 datastore.metric 被直接賦值覆蓋(被 flag,環境變量,set() 方法等等),那麼 datastore.metric 的所有子鍵都將變爲未定義狀態,它們被高優先級配置級別覆蓋了。

如果存在與分隔的鍵路徑匹配的鍵,則直接返回其值。例如:

{
    "datastore.metric.host""0.0.0.0",
    "host"{
        "address""localhost",
        "port"5799
    },
    "datastore"{
        "metric"{
            "host""127.0.0.1",
            "port"3099
        },
        "warehouse"{
            "host""198.0.0.1",
            "port"2112
        }
    }
}

通過 viper.GetString 獲取值:

viper.GetString("datastore.metric.host") // 返回 "0.0.0.0"
  1. 提取子樹

例如:viper 加載瞭如下配置:

app:
  cache1:
    max-items: 100
    item-size: 64
  cache2:
    max-items: 200
    item-size: 80

可以通過 viper.Sub 提取子樹:

subv := viper.Sub("app.cache1")

subv 現在就代表:

max-items: 100
item-size: 64
  1. 反序列化

viper 可以支持將所有或特定的值解析到結構體、map 等。可以通過 2 個函數來實現:

一個示例:

type config struct {
 Port int
 Name string
 PathMap string `mapstructure:"path_map"`
}

var C config

err := viper.Unmarshal(&C)
if err != nil {
 t.Fatalf("unable to decode into struct, %v", err)
}

如果想要解析那些鍵本身就包含 . (默認的鍵分隔符)的配置,則需要修改分隔符:

v := viper.NewWithOptions(viper.KeyDelimiter("::"))

v.SetDefault("chart::values", map[string]interface{}{
    "ingress": map[string]interface{}{
        "annotations": map[string]interface{}{
            "traefik.frontend.rule.type":                 "PathPrefix",
            "traefik.ingress.kubernetes.io/ssl-redirect""true",
        },
    },
})

type config struct {
 Chart struct{
        Values map[string]interface{}
    }
}

var C config

v.Unmarshal(&C)

Viper 在後臺使用 github.com/mitchellh/mapstructure 來解析值,其默認情況下使用 mapstructure tags。當我們需要將 viper 讀取的配置反序列到我們定義的結構體變量中時,一定要使用 mapstructure tags。

  1. 序列化成字符串

有時候我們需要將 viper 中保存的所有設置序列化到一個字符串中,而不是將它們寫入到一個文件中,示例如下:

import (
    yaml "gopkg.in/yaml.v2"
    // ...
)

func yamlStringSettings() string {
    c := viper.AllSettings()
    bs, err := yaml.Marshal(c)
    if err != nil {
        log.Fatalf("unable to marshal config to YAML: %v", err)
    }
    return string(bs)
}

總結

本文討論了 Go 語言中用於加載和解析配置文件的 Viper 包。Viper 支持多種格式的配置文件,並提供了許多功能來幫助開發者管理配置。關鍵要點包括:


本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/iOmHSqAJY7v09Qt_fiXFJg