結構化的 Go 專案設計模式與風格 — 2

Alan Chen
getamis
Published in
5 min readJun 19, 2019

--

Image from https://www.igneous.io/blog/can-you-solve-this-golang-riddle

引言

之前介紹了個人常用的 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/cobragithub.com/spf13/vipergithub.com/oklog/run以及 github.com/uber-go/dig 這幾個套件。

同事們包括我自己在做程式碼審查以及後續開發的時候,發現 cmd 目錄底下的程式碼並不是很好測試,主要原因是很多初始化的函數相依了 viper。當很多測試在平行化執行時,大家共用一個 viper 很容易產生問題。

為了解決上述的問題,試圖做以下的改變:

  1. 每一個執行檔擁有各自的 viper instance。
  2. 每一個初始化函數,儘可能傳一個 Config 物件當作參數,Config 中可以清楚描述該函數需要哪些東西。此外這些值可以透過 dig 的依賴注入自動取得正確的值。

上述的改法的好處是,每一個初始化函數的測試不需要依賴 viper,只需要填好對應的 Config 即可測試。E.g.,

在這個例子中,NewTCPSocket 必須傳入一個 Config 物件,其中包含了 HostPort 兩個資訊,這樣的設計讓我們可以更清楚地知道如何去測試 NewTCPSocket

依賴注入是怎麼完成的呢?在這裏有一半是利用了 dig 中提供的 Named Values 機制,可以參考這裏。剩下要做的事,就是讓 digviper 的設定能夠連結。

以下程式碼提供了這樣的功能,它簡單地封裝了 dig、run、viper、 以及 cobra

pkg/app/types.go
pkg/app/run.go

bindContainer 中,runnerviper 中所有的設定取出,都設定成一個 dig 的 provider function,這樣 viper 的設定就成為了 digNamed Values,之後就可以自動靠依賴注入取得。viper 支援的 Get 系列函數是有限的,所以在這裡用窮舉法列出,似乎無法用 reflection 的方式來處理這段邏輯,如果有還麻煩請教一下小弟 :)

結論

能夠跟著廣大 Gopher 們一起拋棄 dep 改用 Go module,覺得開心 :)

關於 github.com/spf13/cobragithub.com/spf13/vipergithub.com/oklog/run以及 github.com/uber-go/dig 這四個套件的組合,總覺得還能有更簡潔、更易懂的作法。此外也期待這邊能跟 Functional Options Pattern 產生某種火花,日後再慢慢挖掘 XD

--

--