引言
之前介紹了個人常用的 Go 專案設計模式與風格,隨著時間過去,這些風格也不斷地在演變。我一向不是安於現狀的人,對於寫程式可以說有某種程度的潔癖,這是好事也是壞事。最近整理了自己的專案,覺得累積了一些東西,差不多到可以寫一篇文章的程度了。
Go Module
這個版本的一大改變就是從 dep 正式轉換為 Go module。我一直以來堅持一個原則:希望每個專案所用到的工具都由專案自己管理版本。
原本的工具都由 dep 做版本管理,原始碼會放在 vendor 目錄,然後再透過 go build 去建置,接著從 Makefile 去覆蓋掉 PATH 這個環境變數,確保每次呼叫工具時都優先使用專案目錄裡面的版本。轉換成 Go module 以後,沒有了 vendor 目錄。我直接在 tools 資料夾中建立了所需工具的執行檔,以 protoc-gen-go 為例:
在該執行檔中,透過了 go env GOPATH 取得了目前環境中的 GOPATH,然後再指定要執行的套件版本 github.com/golang/protobuf@v1.3.1/protoc-gen-go。這樣就產生了一個 protoc-gen-go 的 wrapper script,接著我們只要讓 protoc 在尋找插件的時候,優先找到這個執行檔即可,一樣透過在 Makefile 中覆蓋 PATH 這個環境變數。
看完了上述的範例,可以發現這個做法有一個明顯的缺點,就是必須在該wrapper script 裡面清楚寫出版本號,因為 Go module 的檔案目錄規則。事實上我們也可以在 wrapper script 中利用 go mod graph 以及剖析 go.mod 的內容動態地去找出版本,不過這就是後話了。核心精神其實就是要讓所有開發同一個專案的人,都使用同樣版本的工具,達到這個目的即可。
Dependency Injection for Configurations
如前一篇文章所提到的,在這個專案中頻繁地使用了 github.com/spf13/cobra、github.com/spf13/viper、github.com/oklog/run以及 github.com/uber-go/dig 這幾個套件。
同事們包括我自己在做程式碼審查以及後續開發的時候,發現 cmd 目錄底下的程式碼並不是很好測試,主要原因是很多初始化的函數相依了 viper。當很多測試在平行化執行時,大家共用一個 viper 很容易產生問題。
為了解決上述的問題,試圖做以下的改變:
- 每一個執行檔擁有各自的 viper instance。
- 每一個初始化函數,儘可能傳一個 Config 物件當作參數,Config 中可以清楚描述該函數需要哪些東西。此外這些值可以透過 dig 的依賴注入自動取得正確的值。
上述的改法的好處是,每一個初始化函數的測試不需要依賴 viper,只需要填好對應的 Config 即可測試。E.g.,
在這個例子中,NewTCPSocket 必須傳入一個 Config 物件,其中包含了 Host 跟 Port 兩個資訊,這樣的設計讓我們可以更清楚地知道如何去測試 NewTCPSocket。
依賴注入是怎麼完成的呢?在這裏有一半是利用了 dig 中提供的 Named Values 機制,可以參考這裏。剩下要做的事,就是讓 dig 跟 viper 的設定能夠連結。
以下程式碼提供了這樣的功能,它簡單地封裝了 dig、run、viper、 以及 cobra:
在 bindContainer 中,runner 把 viper 中所有的設定取出,都設定成一個 dig 的 provider function,這樣 viper 的設定就成為了 dig 的 Named Values,之後就可以自動靠依賴注入取得。viper 支援的 Get 系列函數是有限的,所以在這裡用窮舉法列出,似乎無法用 reflection 的方式來處理這段邏輯,如果有還麻煩請教一下小弟 :)
結論
能夠跟著廣大 Gopher 們一起拋棄 dep 改用 Go module,覺得開心 :)
關於 github.com/spf13/cobra、github.com/spf13/viper、github.com/oklog/run以及 github.com/uber-go/dig 這四個套件的組合,總覺得還能有更簡潔、更易懂的作法。此外也期待這邊能跟 Functional Options Pattern 產生某種火花,日後再慢慢挖掘 XD