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) {...}
這種技術在小規模應用中效果很好,但大型應用可能具有複雜的依賴關係圖,從而導致一個依賴於順序、但本身不那麼有趣的巨大初始化程式碼塊。通常很難乾淨地分解這段程式碼,特別是因為某些依賴項會被多次使用。用另一種實現替換服務的實現可能會很痛苦,因為它需要修改依賴關係圖,新增一整套新的依賴項(及其依賴項…),並刪除未使用的舊依賴項。實際上,在具有大型依賴關係圖的應用中修改初始化程式碼既繁瑣又緩慢。
Wire 等依賴注入工具旨在簡化初始化程式碼的管理。您將服務及其依賴項描述為程式碼或配置,然後 Wire 處理生成的圖以確定順序以及如何向每個服務傳遞所需的內容。透過更改函式簽名或新增或刪除初始化程式來更改應用的依賴項,然後讓 Wire 完成生成整個依賴關係圖初始化程式碼的繁瑣工作。
為什麼這是 Go Cloud 的一部分?
Go Cloud 的目標是透過為有用的雲服務提供慣用的 Go API,使編寫可移植的雲應用變得更容易。例如,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)。
提供者(Providers)是普通的 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)
注入器(Injectors)是生成的函式,它們按依賴順序呼叫提供者。您編寫注入器的簽名,包括作為引數的任何所需輸入,並插入對 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 會很友好地告訴我們相關的行號和型別。我們可以為其新增一個提供者到 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 公司調查問卷
部落格索引