Viper——Go語言寫的配置文件讀取寫入工具神器

Viper——Go語言寫的配置文件讀取寫入工具神器

1. 資料蒐集

一篇中文的使用入門教程:Go語言配置管理神器——Viper中文教程 | 李文周的博客

Viper官方倉庫:spf13/viper: Go configuration with fangs

⚠️ :這裏強烈建議閱讀 官方倉庫的 README.md 和 中文入門教程。

本文提及的多數核心概念將來自以上兩個資料。

本文主要是代碼實踐

2. 新建viper-demo項目

首先我們要建立一個 Go 語言工程測試 Viper 的用法。

mkdir viper-demo
cd viper-demo 
go mod init github.com/xxx/viper-demo
go get github.com/spf13/viper

至此我們新建了一個 Go 語言工程 viper-demo 文件夾. 接下來以此工程爲基礎我們來測試 Viper 的所有用法。

3. 讀取 yaml 文件

viper-demo 文件夾裏 創建兩個文件 :

  • server.yaml
  • main.go

內容分別如下:

server.yaml

name:
  first: panda
  last: 8z
age : 99
hobbies:
  - Coding
  - Movie
  - Swimming

main.go

package main

import (
	"fmt"
	"github.com/spf13/viper"
)

func main(){
  //viper.SetConfigType("yaml") // 如果配置文件的名稱中沒有擴展名,則需要配置此項
  viper.AddConfigPath("./") // 設置讀取路徑:就是在此路徑下搜索配置文件。
  //viper.AddConfigPath("$HOME/.appname")  // 多次調用以添加多個搜索路徑
	viper.SetConfigFile("server.yaml") // 設置被讀取文件的全名,包括擴展名。
  //viper.SetConfigName("server") // 設置被讀取文件的名字: 這個方法 和 SetConfigFile實際上僅使用一個就夠了
	viper.ReadInConfig()  // 讀取配置文件: 這一步將配置文件變成了 Go語言的配置文件對象包含了 map,string 等對象。
	fmt.Println(
		viper.Get("name"), // 過去 配置文件的信息也很容易,用 Get方法。
		viper.Get("age"),
		viper.Get("name.first"),
		viper.Get("hobbies"),
		)
  
  // 控制檯輸出: map[first:panda last:8z] 99 panda [Coding Movie Swimming]
}

3. 寫入配置文件

從配置文件中讀取配置文件是有用的,但是有時你想要存儲在運行時所做的所有修改。爲此,可以使用下面一組命令,每個命令都有自己的用途:

  • WriteConfig - 將當前的viper配置寫入預定義的路徑並覆蓋(如果存在的話)。如果沒有預定義的路徑,則報錯。
  • SafeWriteConfig - 將當前的viper配置寫入預定義的路徑。如果沒有預定義的路徑,則報錯。如果存在,將不會覆蓋當前的配置文件。
  • WriteConfigAs - 將當前的viper配置寫入給定的文件路徑。將覆蓋給定的文件(如果它存在的話)。
  • SafeWriteConfigAs - 將當前的viper配置寫入給定的文件路徑。不會覆蓋給定的文件(如果它存在的話)。

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

示例: 將 main.go 的內容全部改成下面的代碼:

main.go
package main

import (
	"fmt"
	"github.com/spf13/viper"
)

func main(){
	//viper.SetConfigType("yaml") // 如果配置文件的名稱中沒有擴展名,則需要配置此項
	viper.AddConfigPath("./") // 設置讀取路徑:就是在此路徑下搜索配置文件。
	//viper.AddConfigPath("$HOME/.appname")  // 多次調用以添加多個搜索路徑
	viper.SetConfigFile("server.yaml") // 設置被讀取文件的全名,包括擴展名。
	//viper.SetConfigName("server") // 設置被讀取文件的名字: 這個方法 和 SetConfigFile實際上僅使用一個就夠了
	viper.ReadInConfig()  // 讀取配置文件: 這一步將配置文件變成了 Go語言的配置文件對象包含了 map,string 等對象。
	fmt.Println(
		viper.Get("name"), // 過去 配置文件的信息也很容易,用 Get方法。
		viper.Get("age"),
		viper.Get("name.first"),
		viper.Get("hobbies"),
	)
	// 控制檯輸出: map[first:panda last:8z] 99 panda [Coding Movie Swimming]
	viper.WriteConfigAs("new-server.yaml") // 直接寫入,有內容就覆蓋,沒有文件就新建
}

這段代碼執行完,會有一個新的文件出現,文件名: new-server.yaml 內容如下:

age: 99
hobbies:
- Coding
- Movie
- Swimming
name:
  first: panda
  last: 8z

有沒有注意到這個內容被 按字母順序自動排序了。這可能是 Viper自動做了這件事。

探索一下上面的程序使用 SafeWriteConfigAs 方法會怎樣
package main

import (
	"fmt"
	"github.com/spf13/viper"
)

func main(){
	//viper.SetConfigType("yaml") // 如果配置文件的名稱中沒有擴展名,則需要配置此項
	viper.AddConfigPath("./") // 設置讀取路徑:就是在此路徑下搜索配置文件。
	//viper.AddConfigPath("$HOME/.appname")  // 多次調用以添加多個搜索路徑
	viper.SetConfigFile("server.yaml") // 設置被讀取文件的全名,包括擴展名。
	//viper.SetConfigName("server") // 設置被讀取文件的名字: 這個方法 和 SetConfigFile實際上僅使用一個就夠了
	viper.ReadInConfig()  // 讀取配置文件: 這一步將配置文件變成了 Go語言的配置文件對象包含了 map,string 等對象。
	fmt.Println(
		viper.Get("name"), // 過去 配置文件的信息也很容易,用 Get方法。
		viper.Get("age"),
		viper.Get("name.first"),
		viper.Get("hobbies"),
	)
	
	err := viper.SafeWriteConfigAs("new-server.yaml") // 因爲該配置文件已經存在,所以會報錯
	if err != nil {
		fmt.Println(err)
	}
}

控制檯輸出:

map[first:panda last:8z] 99 panda [Coding Movie Swimming]
Config File "new-server.yaml" Already Exists

4. 建立默認值

一個好的配置系統應該支持默認值。鍵不需要默認值,但如果沒有通過配置文件、環境變量、遠程配置或命令行標誌(flag)設置鍵,則默認值非常有用。

重點代碼:

viper.SetDefault("name", "dogger")
viper.SetDefault("age", "18")
viper.SetDefault("class", map[string]string{"class01": "01", "class02": "02"})

完整代碼:

main.go
package main

import (
	"fmt"
	"github.com/spf13/viper"
)

func main(){

	//viper.SetConfigType("yaml") // 如果配置文件的名稱中沒有擴展名,則需要配置此項
	viper.AddConfigPath("./") // 設置讀取路徑:就是在此路徑下搜索配置文件。
	//viper.AddConfigPath("$HOME/.appname")  // 多次調用以添加多個搜索路徑
	viper.SetConfigFile("server.yaml") // 設置被讀取文件的全名,包括擴展名。
	//viper.SetConfigName("server") // 設置被讀取文件的名字: 這個方法 和 SetConfigFile實際上僅使用一個就夠了
	viper.SetDefault("name", "dogger")
	viper.SetDefault("age", "18")
	viper.SetDefault("class", map[string]string{"class01": "01", "class02": "02"})
	
	viper.ReadInConfig()  // 讀取配置文件: 這一步將配置文件變成了 Go語言的配置文件對象包含了 map,string 等對象。
	
	fmt.Println(
		viper.Get("name"), // 過去 配置文件的信息也很容易,用 Get方法。
		viper.Get("age"),
		viper.Get("name.first"),
		viper.Get("hobbies"),
	)
	// 控制檯輸出: map[first:panda last:8z] 99 panda [Coding Movie Swimming]
	viper.WriteConfigAs("server-04.yaml")
}

server.yaml 如文章開頭所示。

新的文件 server-04.yaml是新生成的,內容如下:

server-04.yaml
age: 99
class:
  class01: "01"
  class02: "02"
hobbies:
- Coding
- Movie
- Swimming
name:
  first: panda
  last: 8z

總結:

我們代碼的順序是:

  1. 設置 配置文件搜索路徑
  2. 設置 配置文件名稱
  3. 設置 默認值
  4. 讀取文件
  5. 打印一部分讀取到的值。
  6. 重新寫入文件

能明顯看出讀取後的文件覆蓋了默認設置。

同時也在重新寫入文件這一步看到了 class 被加入了新的配置。

這也驗證了 Viper 對各種值的優先級處理。

5. 優先級

Viper 會按照下面的優先級。每個項目的優先級都高於它下面的項目:

  1. 顯示調用Set設置值
  2. 命令行參數(flag
  3. 環境變量
  4. 配置文件
  5. key/value 存儲
  6. 默認值

重要: 目前 Viper 配置的鍵(Key)是大小寫不敏感的。目前正在討論是否將這一選項設爲可選。

6. key/value 存儲

上一小節我們看了 Viper 處理 配置參數的默認優先級。前文我們已經講了配置文件和默認值兩種方式。

接下來我們將 key/value 存儲。乍一看可能不知道這是什麼東西。

Viper 中啓用遠程支持,需要在代碼中匿名導入viper/remote這個包。

import _ "github.com/spf13/viper/remote"

Viper 將讀取從Key/Value 存儲(例如 etcdConsul)中的路徑檢索到的配置字符串(如JSONTOMLYAMLHCLenvfileJava properties 格式)。這些值的優先級高於默認值,但是會被從磁盤、flag 或環境變量檢索到的配置值覆蓋。(譯註:也就是說 Viper 加載配置值的優先級爲:磁盤上的配置文件 > 命令行參數 > 環境變量 > 遠程 **Key/Value 存儲 ** > 默認值。)

Viper使用 cryptK/V 存儲 中檢索配置,這意味着如果你有正確的 gpg 密匙,你可以將配置值加密存儲並自動解密。加密是可選的。

你可以將遠程配置與本地配置結合使用,也可以獨立使用。

crypt 有一個命令行助手,你可以使用它將配置放入 K/V 存儲中crypt 默認使用在http://127.0.0.1:4001的etcd。

$ go get github.com/bketelsen/crypt/bin/crypt
$ crypt set -plaintext /config/hugo.json /Users/hugo/settings/config.json

確認值已經設置:

$ crypt get -plaintext /config/hugo.json

有關如何設置加密值或如何使用Consul的示例,請參見 crypt 文檔。

總結:

對於分佈式場景和微服務場景中可能會用到。方式方法幾乎和我們前文測試的和從配置文件讀取一樣。

涉及到 etcd 或 Consul 等外部組件,這裏不額外做代碼測試。

僅在下方搬運李文周先生博客裏的代碼片段

遠程Key/Value存儲示例-未加密
etcd
viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001","/config/hugo.json")
viper.SetConfigType("json") // 因爲在字節流中沒有文件擴展名,所以這裏需要設置下類型。支持的擴展名有 "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv"
err := viper.ReadRemoteConfig()
Consul

你需要 Consul Key/Value存儲中設置一個Key保存包含所需配置的JSON值。例如,創建一個keyMY_CONSUL_KEY將下面的值存入Consul key/value 存儲:

{
    "port": 8080,
    "hostname": "liwenzhou.com"
}
viper.AddRemoteProvider("consul", "localhost:8500", "MY_CONSUL_KEY")
viper.SetConfigType("json") // 需要顯示設置成json
err := viper.ReadRemoteConfig()

fmt.Println(viper.Get("port")) // 8080
fmt.Println(viper.Get("hostname")) // liwenzhou.com
Firestore
viper.AddRemoteProvider("firestore", "google-cloud-project-id", "collection/document")
viper.SetConfigType("json") // 配置的格式: "json", "toml", "yaml", "yml"
err := viper.ReadRemoteConfig()

當然,你也可以使用SecureRemoteProvider

遠程Key/Value存儲示例-加密
viper.AddSecureRemoteProvider("etcd","http://127.0.0.1:4001","/config/hugo.json","/etc/secrets/mykeyring.gpg")
viper.SetConfigType("json") // 因爲在字節流中沒有文件擴展名,所以這裏需要設置下類型。支持的擴展名有 "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv"
err := viper.ReadRemoteConfig()
監控etcd中的更改-未加密
// 或者你可以創建一個新的viper實例
var runtime_viper = viper.New()

runtime_viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001", "/config/hugo.yml")
runtime_viper.SetConfigType("yaml") // 因爲在字節流中沒有文件擴展名,所以這裏需要設置下類型。支持的擴展名有 "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv"

// 第一次從遠程讀取配置
err := runtime_viper.ReadRemoteConfig()

// 反序列化
runtime_viper.Unmarshal(&runtime_conf)

// 開啓一個單獨的goroutine一直監控遠端的變更
go func(){
	for {
	    time.Sleep(time.Second * 5) // 每次請求後延遲一下

	    // 目前只測試了etcd支持
	    err := runtime_viper.WatchRemoteConfig()
	    if err != nil {
	        log.Errorf("unable to read remote config: %v", err)
	        continue
	    }

	    // 將新配置反序列化到我們運行時的配置結構體中。你還可以藉助channel實現一個通知系統更改的信號
	    runtime_viper.Unmarshal(&runtime_conf)
	}
}()

7. 顯示調用Set設置值

覆蓋設置

這些可能來自命令行標誌,也可能來自你自己的應用程序邏輯。

viper.Set("Verbose", true)
viper.Set("LogFile", LogFile)

8. 命令行參數(flag

Viper 具有綁定到標誌的能力。具體來說,Viper支持Cobra庫中使用的Pflag

BindEnv類似,該值不是在調用綁定方法時設置的,而是在訪問該方法時設置的。這意味着你可以根據需要儘早進行綁定,即使在init()函數中也是如此。

對於單個標誌,BindPFlag()方法提供此功能。

例如:

serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))
綁定一組現有的pflags

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

舉個例子:

pflag.Int("flagname", 1234, "help message for flagname")

pflag.Parse()
viper.BindPFlags(pflag.CommandLine)

i := viper.GetInt("flagname") // 從viper而不是從pflag檢索值

在 Viper 中使用 pflag 並不阻礙其他包中使用標準庫中的 flag 包。pflag 包可以通過導入這些 flags 來處理flag包定義的flags。這是通過調用pflag包提供的便利函數AddGoFlagSet()來實現的。

例如:

package main

import (
	"flag"
	"github.com/spf13/pflag"
)

func main() {

	// 使用標準庫 "flag" 包
	flag.Int("flagname", 1234, "help message for flagname")

	pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
	pflag.Parse()
	viper.BindPFlags(pflag.CommandLine)

	i := viper.GetInt("flagname") // 從 viper 檢索值

	...
}
flag接口

如果你不使用Pflag,Viper 提供了兩個Go接口來綁定其他 flag 系統。

FlagValue表示單個flag。這是一個關於如何實現這個接口的非常簡單的例子:

type myFlag struct {}
func (f myFlag) HasChanged() bool { return false }
func (f myFlag) Name() string { return "my-flag-name" }
func (f myFlag) ValueString() string { return "my-flag-value" }
func (f myFlag) ValueType() string { return "string" }

一旦你的 flag 實現了這個接口,你可以很方便地告訴Viper綁定它:

viper.BindFlagValue("my-flag-name", myFlag{})

FlagValueSet代表一組 flags 。這是一個關於如何實現這個接口的非常簡單的例子:

type myFlagSet struct {
	flags []myFlag
}

func (f myFlagSet) VisitAll(fn func(FlagValue)) {
	for _, flag := range flags {
		fn(flag)
	}
}

一旦你的flag set實現了這個接口,你就可以很方便地告訴Viper綁定它:

fSet := myFlagSet{
	flags: []myFlag{myFlag{}, myFlag{}},
}
viper.BindFlagValues("my-flags", fSet)

9. 環境變量

Viper完全支持環境變量。這使Twelve-Factor App開箱即用。有五種方法可以幫助與ENV協作:

  • AutomaticEnv()
  • BindEnv(string...) : error
  • SetEnvPrefix(string)
  • SetEnvKeyReplacer(string...) *strings.Replacer
  • AllowEmptyEnv(bool)

使用ENV變量時,務必要意識到Viper將ENV變量視爲區分大小寫。

Viper提供了一種機制來確保ENV變量是惟一的。通過使用SetEnvPrefix,你可以告訴Viper在讀取環境變量時使用前綴。BindEnvAutomaticEnv都將使用這個前綴。

BindEnv使用一個或兩個參數。第一個參數是鍵名稱,第二個是環境變量的名稱。環境變量的名稱區分大小寫。如果沒有提供ENV變量名,那麼Viper將自動假設ENV變量與以下格式匹配:前綴+ “_” +鍵名全部大寫。當你顯式提供ENV變量名(第二個參數)時,它 不會 自動添加前綴。例如,如果第二個參數是“id”,Viper將查找環境變量“ID”。

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

AutomaticEnv是一個強大的助手,尤其是與SetEnvPrefix結合使用時。調用時,Viper會在發出viper.Get請求時隨時檢查環境變量。它將應用以下規則。它將檢查環境變量的名稱是否與鍵匹配(如果設置了EnvPrefix)。

SetEnvKeyReplacer允許你使用strings.Replacer對象在一定程度上重寫 Env 鍵。如果你希望在Get()調用中使用-或者其他什麼符號,但是環境變量裏使用_分隔符,那麼這個功能是非常有用的。可以在viper_test.go中找到它的使用示例。

或者,你可以使用帶有NewWithOptions工廠函數的EnvKeyReplacer。與SetEnvKeyReplacer不同,它接受StringReplacer接口,允許你編寫自定義字符串替換邏輯。

默認情況下,空環境變量被認爲是未設置的,並將返回到下一個配置源。若要將空環境變量視爲已設置,請使用AllowEmptyEnv方法。

Env 示例:
SetEnvPrefix("spf") // 將自動轉爲大寫
BindEnv("id")

os.Setenv("SPF_ID", "13") // 通常是在應用程序之外完成的

id := Get("id") // 13

10.監控並重新讀取配置文件

Viper支持在運行時實時讀取配置文件的功能。

需要重新啓動服務器以使配置生效的日子已經一去不復返了,viper驅動的應用程序可以在運行時讀取配置文件的更新,而不會錯過任何消息。

只需告訴viper實例watchConfig。可選地,你可以爲Viper提供一個回調函數,以便在每次發生更改時運行。

確保在調用WatchConfig()之前添加了所有的配置路徑。

viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
  // 配置文件發生變更之後會調用的回調函數
	fmt.Println("Config file changed:", e.Name)
})
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章