Go 部落格
使用 Go Cloud 的 Wire 實現編譯時依賴注入
概覽
Go 團隊最近宣佈了開源專案Go Cloud,該專案提供了可移植的 Cloud API 和用於開放雲開發的工具。本文將詳細介紹 Wire,一個在 Go Cloud 中使用的依賴注入工具。
Wire 解決了什麼問題?
依賴注入是一種透過顯式地為元件提供其工作所需的所有依賴項來生成靈活且松耦合程式碼的標準技術。在 Go 中,這通常以將依賴項傳遞給建構函式的形式出現。
// NewUserStore returns a UserStore that uses cfg and db as dependencies.
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}
這項技術在小規模應用中效果很好,但大型應用程式可能有複雜的依賴關係圖,導致出現一大塊依賴於順序但除此之外卻並不那麼有趣(interesting)的初始化程式碼。由於某些依賴項會被多次使用,因此很難乾淨地分解這段程式碼。將服務的某個實現替換為另一個實現可能會很痛苦,因為它涉及到修改依賴關係圖,新增一整套新的依賴項(及其依賴項……),並刪除未使用的舊依賴項。實際上,在具有大型依賴關係圖的應用程式中,對初始化程式碼進行更改既乏味又緩慢。
像 Wire 這樣的依賴注入工具旨在簡化初始化程式碼的管理。您可以將服務及其依賴項描述為程式碼或配置,然後 Wire 會處理生成的依賴關係圖,以確定順序以及如何將每個服務所需的內容傳遞給它。透過更改函式簽名或新增/刪除初始化程式來更改應用程式的依賴項,然後讓 Wire 完成為整個依賴關係圖生成初始化程式碼的繁瑣工作。
為什麼這是 Go Cloud 的一部分?
Go Cloud 的目標是透過為有用的 Cloud 服務提供符合 Go 習慣的 API,使編寫可移植的 Cloud 應用程式變得更容易。例如,blob.Bucket 提供了一個儲存 API,其中包含 Amazon S3 和 Google Cloud Storage (GCS) 的實現;使用 blob.Bucket
編寫的應用程式可以在不更改其應用程式邏輯的情況下交換實現。然而,初始化程式碼本質上是特定於提供商的,並且每個提供商都有不同的依賴項集。
例如,構造 GCS blob.Bucket
需要一個 gcp.HTTPClient
,它最終需要 google.Credentials
;而構造 S3 的 blob.Bucket
需要一個 aws.Config
,它最終需要 AWS 憑證。因此,更新應用程式以使用不同的 blob.Bucket
實現涉及到我們上面描述的那種繁瑣的依賴關係圖更新。Wire 的主要用例是簡化 Go Cloud 可移植 API 實現的交換,但它也是一個通用的依賴注入工具。
以前沒有做過嗎?
市面上有許多依賴注入框架。對於 Go,Uber 的 dig 和Facebook 的 inject 都使用反射來進行執行時依賴注入。Wire 主要受到 Java 的Dagger 2 的啟發,並使用程式碼生成而不是反射或服務定位器。
我們認為這種方法有幾個優點:
- 當依賴關係圖變得複雜時,執行時依賴注入可能難以理解和除錯。使用程式碼生成意味著執行時執行的初始化程式碼是常規的、符合 Go 習慣的程式碼,易於理解和除錯。沒有什麼會被一個進行“魔法”操作的中間框架所混淆。特別是,諸如忘記依賴項之類的問題會變成編譯時錯誤,而不是執行時錯誤。
- 與服務定位器不同,無需建立任意的名稱或鍵來註冊服務。Wire 使用 Go 型別來連線元件與其依賴項。
- 更容易避免依賴膨脹。Wire 生成的程式碼將僅匯入您需要的依賴項,因此您的二進位制檔案不會有未使用的匯入。執行時依賴注入器無法在執行時識別未使用的依賴項。
- Wire 的依賴關係圖是靜態可知的,這為工具和視覺化提供了機會。
它是如何工作的?
Wire 包含兩個基本概念:提供程式(providers)和注入器(injectors)。
提供程式是普通的 Go 函式,它們在給定其依賴項的情況下“提供”值,這些依賴項僅作為函式的引數來描述。這裡有一些定義三個提供程式的示例程式碼:
// NewUserStore is the same function we saw above; it is a provider for UserStore,
// with dependencies on *Config and *mysql.DB.
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}
// NewDefaultConfig is a provider for *Config, with no dependencies.
func NewDefaultConfig() *Config {...}
// NewDB is a provider for *mysql.DB based on some connection info.
func NewDB(info *ConnectionInfo) (*mysql.DB, error) {...}
可以組合使用常用提供程式並將其分組到 ProviderSets
中。例如,在建立 *UserStore
時,通常會使用預設的 *Config
,因此我們可以將 NewUserStore
和 NewDefaultConfig
分組到一個 ProviderSet
中。
var UserStoreSet = wire.ProviderSet(NewUserStore, NewDefaultConfig)
注入器是生成的函式,它們按依賴順序呼叫提供程式。您編寫注入器的簽名,包括任何需要的輸入作為引數,並插入一個對 wire.Build
的呼叫,其中包含構建最終結果所需的提供程式或提供程式集的列表。
func initUserStore() (*UserStore, error) {
// We're going to get an error, because NewDB requires a *ConnectionInfo
// and we didn't provide one.
wire.Build(UserStoreSet, NewDB)
return nil, nil // These return values are ignored.
}
現在我們執行 go generate 來執行 wire。
$ go generate
wire.go:2:10: inject initUserStore: no provider found for ConnectionInfo (required by provider of *mysql.DB)
wire: generate failed
糟糕!我們沒有包含 ConnectionInfo
,也沒有告訴 Wire 如何構建它。Wire 很有幫助地告訴我們涉及的行號和型別。我們可以將它(ConnectionInfo
)的提供程式新增到 wire.Build
中,或者將其作為引數新增。
func initUserStore(info ConnectionInfo) (*UserStore, error) {
wire.Build(UserStoreSet, NewDB)
return nil, nil // These return values are ignored.
}
現在 go generate
將建立一個新檔案,其中包含生成的程式碼。
// File: wire_gen.go
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
func initUserStore(info ConnectionInfo) (*UserStore, error) {
defaultConfig := NewDefaultConfig()
db, err := NewDB(info)
if err != nil {
return nil, err
}
userStore, err := NewUserStore(defaultConfig, db)
if err != nil {
return nil, err
}
return userStore, nil
}
任何非注入器宣告都會被複制到生成的檔案中。執行時沒有對 Wire 的依賴:所有編寫的程式碼都只是普通的 Go 程式碼。
正如您所見,輸出非常接近開發人員自己會編寫的內容。這是一個只有三個元件的簡單示例,所以手動編寫初始化程式不會太痛苦,但對於擁有更復雜依賴關係圖的元件和應用程式,Wire 可以節省大量的手動工作。
我如何參與和了解更多資訊?
Wire README 詳細介紹瞭如何使用 Wire 及其更高階的功能。還有一個教程,它將指導您在簡單應用程式中使用 Wire。
我們非常歡迎您分享使用 Wire 的經驗!Wire 的開發在 GitHub 上進行,因此您可以提交 issue 來告訴我們哪些方面可以改進。有關該專案的更新和討論,請加入Go Cloud 郵件列表。
感謝您花時間瞭解 Go Cloud 的 Wire。我們很高興與您合作,使 Go 成為構建可移植雲應用程式開發人員的首選語言。
下一篇文章:宣佈推出 App Engine 新的 Go 1.11 執行時
上一篇文章:參與 2018 Go 公司問卷調查
部落格索引