在平常項目的開發中,咱們常常會使用配置文件來保存項目的基本元數據,配置文件的類型有不少,如:JSON
、xml
、yaml
、甚至多是個純文本格式的文件。不論是什麼類型的配置數據,在某些場景下,咱們可會有熱更新當前配置文件內容的需求,好比:使用Go運行的一個常駐進程,運行了一個 Web Server
服務進程。git
此時,若是配置文件發生變化,咱們如何讓當前程序從新讀取新的配置文件內容呢?接下來,咱們將使用以下兩種方式實現配置文件的更新:github
inotify
, 監聽文件修改事件。不論是哪種方式,都會用到Go語言中 goroutine
的概念,我打算使用 goroutine
新起一個協程,新協程的目的是用來接收系統信號(signal)或者監聽文件被修改的事件,若是你對 goroutine
的概念不是很瞭解,那麼建議你先查閱相關資料。json
我之因此稱這種方式爲手動式(Manual
),是由於文件的更新是須要咱們本身去手動告知當前依賴的運行程序:"嘿,哥們!配置文件更新啦,你得從新讀一下配置內容!!",咱們告知的方式就是向當前運行程序發送一個系統信號,所以程序的大概思路以下:微信
goroutine
,用來接收信號。在 *nix
系統中規定,USR1
和USR2
均屬於用戶自定義信號,至於USR1
和 USR2
哪個更合,維基百科 也沒有給出權威的答案,因此在這裏我按約定俗稱的規矩,打算使用USR1
:數據結構
若是你使用過Nginx或者Apache等Web Server,那麼你對採用發送信號更新配置文件的策略確定多少有點印象。併發
在Go
語言中監聽系統信號須要使用 signal
包Notify()
方法,該方法至少須要兩個參數,第一個參數要求是一個系統信號類型的通道,後續參數爲一個或多個須要監聽的系統信號:app
import "os/signal" Notify(c chan<- os.Signal, sig ...os.Signal)
所以,咱們的代碼大體以下:函數
package main import ( "os" "os/signal" "syscall" ) func main() { // 聲明一個容量爲1的信號通道 sig := make(chan os.Signal, 1) // 監聽系統SIGUSR1發出的信號 signal.Notify(sig, syscall.SIGUSR1) }
在這裏咱們建立了一個信號容量大小爲1的通道(channel
),這表示,通道里最多能容納下1個信號單元,若是當前通道里已經存在一個信號單元,此時又接收到另外一個信號須要發送到通道中,那麼在發送該信號的時候程序會被阻塞,直到通道里的信號被處理掉。優化
經過這種方式,咱們能夠一次精確的只處理一個信號,多個信號都須要排隊的目的,這正是我想要的效果。spa
當系統信號被監聽存入通道後(sig
中),接下來咱們須要處理接收到到信號,這裏咱們新起的協程(goroutine),使用協程的目的是但願後續的任務不阻塞主進程的運行,在 GO
語言中,另起一個協程是很是方便的,只須要調用關鍵字:go
便可:
go func(){ // 新線程 }()
咱們但願在新協程中永不停歇的獲取通道中的系統信號,代碼以下:
go func() { for { select { case <-sig: // 獲取通道中的信號,處理信號 } } }()
GO
語言中的select
語句,其結構有點相似於其餘語言的switch
語句,但不一樣的是,select
只能被用來處理 goroutine
的通信操做,而goroutine
的通信又是基於channel
來實現的,因此直白點說:select
只能用來處理通道(channel
)的操做。
當前的select
一直會處於阻塞狀態,直到它的某個case
符合條件時纔會執行該case
條件下的語句。而且此處咱們使用了for
循環結構,讓select
語句處於一個無限循環當中,若是select
下的case
接收到一個處理的信號後,當處理結束後;因爲外層for
循環的語句的做用,至關於重置了select
的狀態,在沒有接收到新的信號時,select
將再次被阻塞等待,循環往復。
若是你對select
語句的阻塞有疑問,咱們不妨考慮下面代碼的運行狀況:
for { select { case <-sig: // 獲取通道中的信號,處理信號 } fmt.Println("select block test!") }
在如上的select
語句後,咱們嘗試輸出一行字符串,那麼請問:"這行fmt.Println()
函數會在for
循環中當即運行嗎?"
答案是確定的:不會!select
會阻塞調,當程序運行起來時不會有任何輸出,直到case
匹配到。你不妨試試。
咱們已經準備好了信號的監聽,以及信號處理的簡單工做,接下來咱們須要細化信號處理階段的代碼,須要添加上加載配置文件的邏輯,咱們將演示加載一份簡單的json
配置文件,文件的路徑存放於/tmp/env.json
,內容比較簡單,僅一個test
字段:
{ "test": "D" }
同時,咱們須要建立解析該json
格式配套的數據結構:
type configBean struct { Test string }
咱們聲明瞭一個configBean
結構體,用來和env.json
配置文件字段一一映射,而後只要調用json.Unmarshal()
函數,咱們就能夠把這份json
文件內容轉爲對應的Go
語言結構體內容,固然這還不夠,在解析完以後咱們還須要聲明一個變量來存儲這份結構體數據,供程序在其餘地方調用:
// 全局配置變量 var Config config type config struct { LastModify time.Time Data configBean }
此處,我並無直接把configBean
解析的json
數據賦值給全局變量,而是又包裝了一層,額外聲明瞭一個字段 LastModify
用來存儲當前文件的最後一次修改時間,這樣的好處在於,咱們每收到一個須要更新配置文件的信號時,咱們還須要比對當前文件的修改是否大於上一次的更新時間,固然這僅僅是一個配置優化加載的小技巧。
以下即是咱們的加載配置文件的代碼,這裏新增了一個loadConfig(path string)
函數,用於封裝加載配置文件的全部邏輯:
// 全局配置變量 var Config *config type configBean struct { Test string } type config struct { LastModify time.Time Data configBean // 配置內容存儲字段 } func loadConfig(path string) error { var locker = new(sync.RWMutex) // 讀取配置文件內容 data, err := ioutil.ReadFile(path) if err != nil { return err } // 讀取文件屬性 fileInfo, err := os.Stat(path) if err != nil { return err } // 驗證文件的修改時間 if Config != nil && fileInfo.ModTime().Before(Config.LastModify) { return errors.New("no need update") } // 解析文件內容 var configBean configBean err = json.Unmarshal(data, &configBean) if err != nil { return err } config := config{ LastModify: fileInfo.ModTime(), Data: configBean, } // 從新賦值更新配置文件 locker.Lock() Config = config locker.Unlock() return nil }
關於loadConfig()
函數咱們須要說明的是,此處咱們雖然使用了鎖,可是在文件讀寫並沒使用鎖,僅在賦值階段使用,由於在這種場景下不存在多個goroutine
同時操做同一個文件的需求,若是你所在的場景存在多個goroutine
併發寫操做,那麼保險起見,建議你把文件的讀寫最好也加上鎖機制。
至此,咱們大體完成了利用監聽系統信號更新配置文件的全部全部邏輯,接下來咱們來演示最終成果,演示以前咱們還需在main
函數添加一點額外代碼,模擬主進程成爲一個常駐進程,這裏仍是使用通道,最後代碼大體以下:
func main() { configPath := "/tmp/env.json" done := make(chan bool, 1) // 定義信號通道 sig := make(chan os.Signal, 1) signal.Notify(sig, syscall.SIGUSR1) go func(path string) { for { select { case <-sig: // 收到信號, 加載配置文件 _ := loadConfig(path) } } }(configPath) // 掛起進程,直到獲取到一個信號 <-done }
最終咱們使用一張gif圖片來演示最終效果:
最終的完整版代碼,請在此處查看:github代碼地址,而且須要說明的是,demo
中的代碼還有些小細節,例如:錯誤的處理,信號通道的關閉等,請自行處理。
預告:鑑於文章篇幅考慮,本文中咱們只實現了第一種文件更新方式。下一篇文章中,咱們將使用第二種方式:使用inotify
監聽配置文件的變化,以實現配置文件的自動更新,期待你的關注。
(360技術原創內容,轉載請務必保留文末二維碼,謝謝~)
關於360技術
360技術是360技術團隊打造的技術分享公衆號,天天推送技術乾貨內容
更多技術信息歡迎關注「360技術」微信公衆號