上一篇文章中我們學會了使用包管理工具,這樣我們就可以很方便的使用包管理工具來管理我們依賴的包。
配置工具的選擇
但我們又遇到了一個問題,一個項目通常是有很多配置的,比如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-action/tree/master/ch3
參考資料
Design Patterns in Golang: Singleton
Golang hot configuration reload
Golang中的信號處理