Go 部落格
利用 Go Cloud 中的 Wire 實作編譯期依賴注入
概述
Go 團隊最近 宣布 開放原始碼專案 Go 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 的目標是提供有用的 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 有兩個基本概念:提供者和注入器。
提供者 是普通的 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
,因此我們可以在 ProviderSet
中將 NewUserStore
和 NewDefaultConfig
分組
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 會貼心地告訴我們涉及的程式碼行號和類型。我們可以將提供者新增到 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 上進行開發,所以您可以建立一個議題,告訴我們什麼地方可以做得更好。若要取得專案的更新和討論,請加入Go Cloud 郵件清單。
謝謝您撥冗了解 Go Cloud 的 Wire。我們熱切期待與您攜手合作,讓 Go 成為打造可攜式雲端應用程式的開發人員首選語言。
下一篇文章:宣布 App Engine 的新版 Go 1.11 執行時期
上一篇文章:參與 2018 年 Go 公司問卷調查
部落格索引