TOML 配置處理

CraryPrimitiveMan發表於2018-05-21

文章來源:Golang學習--TOML配置處理

上一篇文章中我們學會了使用包管理工具,這樣我們就可以很方便的使用包管理工具來管理我們依賴的包。

配置工具的選擇

但我們又遇到了一個問題,一個專案通常是有很多配置的,比如PHP的php.ini檔案、Nginx的server.conf檔案,那麼Golang的專案又適合使用怎樣的配置檔案呢?

其實現在我們有很多選擇,比如 JSON檔案、INI檔案、YAML檔案和TOML檔案等等。

其中這些檔案,對應的Golang處理庫如下:

  • encoding/json -- 標準庫中的包,可以處理JSON配置檔案,缺點是不能加註釋
  • gcfg -- 處理INI配置檔案
  • toml -- 處理TOML配置檔案
  • viper -- 處理JSON, TOML, YAML, HCL以及Java properties配置檔案

其實關於怎麼選擇可以看看stackoverflow上的問題How to handle configuration in Go

toml的使用

我根據自己的喜好選了toml,下面就來說下toml。

先來看一個TOML檔案的例子:

# This is a TOML document.

title = "TOML Example"

[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00 # First class dates

[database]
server = "192.168.1.1"
ports = [ 8001, 8001, 8002 ]
connection_max = 5000
enabled = true

[servers]

  # Indentation (tabs and/or spaces) is allowed but not required
  [servers.alpha]
  ip = "10.0.0.1"
  dc = "eqdc10"

  [servers.beta]
  ip = "10.0.0.2"
  dc = "eqdc10"

[clients]
data = [ ["gamma", "delta"], [1, 2] ]

# Line breaks are OK when inside arrays
hosts = [
  "alpha",
  "omega"
]

大家可以看到這裡的格式非常靈活,可以是數字、字串、布林等簡單型別,也可以是陣列、map等等複雜的型別。

關於具體的TOML語言的解說大家檢視文件 toml-lang/toml

下面我們再來說一下,具體的Golang程式碼中如何使用

我們基於上面的配置檔案來定義Golang中配置的struct,如下:

type tomlConfig struct {
    Title string
    Owner ownerInfo
    DB database `toml:"database"`
    Servers map[string]server
    Clients clients
}

type ownerInfo struct {
    Name string
    Org string `toml:"organization"`
    Bio string
    DOB time.Time
}

type database struct {
    Server string
    Ports []int
    ConnMax int `toml:"connection_max"`
    Enabled bool
}

type server struct {
    IP string
    DC string
}

type clients struct {
    Data [][]interface{}
    Hosts []string
}

這一些都定義好之後,我們只需要將檔案配置中的內容轉成Golang中可用的struct例項即可,程式碼如下:

var config tomlConfig
filePath := "/your/path/config.toml"
if _, err := toml.DecodeFile(filePath, &config); err != nil {
    panic(err)
}

這樣我們拿到的config就是擁有TOML檔案內容的tomlConfig的例項,可以直接使用。

配置的單例模式

通常來說,在一個專案中,配置檔案只需要解析一次,所以可以使用單例模式包一下config的解析。

程式碼如下:

package config

var (
    cfg * tomlConfig
    once sync.Once
)

func Config() *tomlConfig {
    once.Do(func() {
        filePath, err := filepath.Abs("./ch3/config.toml")
        if err != nil {
            panic(err)
        }
        fmt.Printf("parse toml file once. filePath: %s\n", filePath)
        if _ , err := toml.DecodeFile(filePath, &cfg); err != nil {
            panic(err)
        }
    })
    return cfg
}

這裡我們使用了sync.Once的Do方法,Do方法當且僅當第一次被呼叫時才執行函式。

如果once.Do(f)被多次呼叫,只有第一次呼叫會執行f,即使f每次呼叫Do 提供的f值不同。需要給每個要執行僅一次的函式都建立一個Once型別的例項。

這樣我們就保證了tomlConfig物件是一個單例模式,只需要解析一次,可以在任何地方呼叫。呼叫例子如下:

// 配置中DB的IP
fmt.Println(config.Config().DB.Server)
// 配置中Owner的名字
fmt.Println(config.Config().Owner.Name)

配置的更新

如果我們的專案是一個常駐的專案(比如http server),我們會希望能夠提供更新配置的功能,平滑的替換掉配置,不需要重啟專案。

其實思路很想簡單,我們只需要起一個協程,監視我們定義好的訊號,如果接收到訊號就重新載入配置。

下面我們來寫下,更新配置的程式碼:

    s := make(chan os.Signal, 1)
    signal.Notify(s, syscall.SIGUSR1)
    go func() {
        for {
            <-s
            config.ReloadConfig()
            log.Println("Reloaded config")
        }
    }()

我們監視了syscall.SIGUSR1訊號,其值是30,接收到訊號就執行config.ReloadConfig()方法。

再來看下config中方法變動:

func Config() *tomlConfig {
    once.Do(ReloadConfig)
    cfgLock.RLock()
    defer cfgLock.RUnlock()
    return cfg
}

func ReloadConfig() {
    filePath, err := filepath.Abs("./ch3/config.toml")
    if err != nil {
        panic(err)
    }
    fmt.Printf("parse toml file once. filePath: %s\n", filePath)
    config := new(tomlConfig)
    if _ , err := toml.DecodeFile(filePath, config); err != nil {
        panic(err)
    }
    cfgLock.Lock()
    defer cfgLock.Unlock()
    cfg = config
}

原來載入配置的程式碼放到ReloadConfig方法中去了,還在給變數cfg賦值的時候加了讀寫鎖,以保證安全。在Config方法中獲取cfg的時候加了讀鎖,防止在讀的時候,也在寫入,導致配置錯亂。

啟動server之後,可以通過如下shell命令更新配置

kill -SIGUSR1 6666

其中的6666是go server的程式號。執行這條命令之後,會向go server傳送syscall.SIGUSR1的訊號,從而觸發更新配置的動作。

POSIX訊號

這邊順便列一下POSIX中定義的訊號:

Linux 使用34-64訊號用作實時系統中。

命令 man 7 signal 提供了官方的訊號介紹。

在POSIX.1-1990標準中定義的訊號列表:

訊號 動作 說明
SIGHUP 1 Term 終端控制程式結束(終端連線斷開)
SIGINT 2 Term 使用者傳送INTR字元(Ctrl+C)觸發
SIGQUIT 3 Core 使用者傳送QUIT字元(Ctrl+/)觸發
SIGILL 4 Core 非法指令(程式錯誤、試圖執行資料段、棧溢位等)
SIGABRT 6 Core 呼叫abort函式觸發
SIGFPE 8 Core 算術執行錯誤(浮點運算錯誤、除數為零等)
SIGKILL 9 Term 無條件結束程式(不能被捕獲、阻塞或忽略)
SIGSEGV 11 Core 無效記憶體引用(試圖訪問不屬於自己的記憶體空間、對只讀記憶體空間進行寫操作)
SIGPIPE 13 Term 訊息管道損壞(FIFO/Socket通訊時,管道未開啟而進行寫操作)
SIGALRM 14 Term 時鐘定時訊號
SIGTERM 15 Term 結束程式(可以被捕獲、阻塞或忽略)
SIGUSR1 30,10,16 Term 使用者保留
SIGUSR2 31,12,17 Term 使用者保留
SIGCHLD 20,17,18 Ign 子程式結束(由父程式接收)
SIGCONT 19,18,25 Cont 繼續執行已經停止的程式(不能被阻塞)
SIGSTOP 17,19,23 Stop 停止程式(不能被捕獲、阻塞或忽略)
SIGTSTP 18,20,24 Stop 停止程式(可以被捕獲、阻塞或忽略)
SIGTTIN 21,21,26 Stop 後臺程式從終端中讀取資料時觸發
SIGTTOU 22,22,27 Stop 後臺程式向終端中寫資料時觸發

在SUSv2和POSIX.1-2001標準中的訊號列表:

訊號 動作 說明
SIGTRAP 5 Core Trap指令觸發(如斷點,在偵錯程式中使用)
SIGBUS 0,7,10 Core 非法地址(記憶體地址對齊錯誤)
SIGPOLL Term Pollable event (Sys V). Synonym for SIGIO
SIGPROF 27,27,29 Term 效能時鐘訊號(包含系統呼叫時間和程式佔用CPU的時間)
SIGSYS 12,31,12 Core 無效的系統呼叫(SVr4)
SIGURG 16,23,21 Ign 有緊急資料到達Socket(4.2BSD)
SIGVTALRM 26,26,28 Term 虛擬時鐘訊號(程式佔用CPU的時間)(4.2BSD)
SIGXCPU 24,24,30 Core 超過CPU時間資源限制(4.2BSD)
SIGXFSZ 25,25,31 Core 超過檔案大小資源限制(4.2BSD)

程式碼可參考:https://github.com/CraryPrimitiveMan/go-in...

參考資料

Design Patterns in Golang: Singleton
Golang hot configuration reload
Golang中的訊號處理

本作品採用《CC 協議》,轉載必須註明作者和本文連結

Talk is cheap. Show me the code.

相關文章