[譯]使用Go Cloud的Wire進行編譯時依賴注入

一桶冷水發表於2019-03-04

2018年10月9日

概述

Go團隊最近公佈了用於開放雲開發的可移植雲API和工具,開源專案Go Cloud 。 這篇文章詳細介紹了Wire,一個隨Go Cloud提供的依賴注入工具。

Wire解決了什麼問題?

依賴注入是一種編寫可伸縮、低耦合程式碼的標準技術。因為依賴注入顯式地為元件提供他們需要工作的所有依賴關係。 在Go中,這通常採用將依賴項傳遞給建構函式的形式:

 // NewUserStore返回一個使用cfg和db作為依賴項的UserStore。
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}
複製程式碼

這種技術在小規模下工作得很好,但是較大的應用程式會存在一個複雜的依賴圖。這導致了一大塊依賴於順序的初始化程式碼,這並不好玩。 因為一些依賴項被多次使用,通常很難乾淨地拆分這些程式碼。 將服務的一個實現替換為另一個實現也會很痛苦,因為這涉及到通過新增一組全新的依賴項(及其依賴項…)來修改依賴項圖,並刪除未使用的舊項。 實際上,在具有龐大依賴圖的應用程式中更改初始化程式碼是繁瑣且緩慢的。

像Wire這樣的依賴注入工具旨在簡化初始化程式碼的管理。您可以將您的服務及其依賴關係描述為程式碼或配置,然後Wire處理生成關係圖,再據此確定初始化排序以及如何向每個服務傳遞它所需的依賴。 通過更改函式簽名、新增或刪除初始化程式來更改應用程式的依賴項,然後讓Wire執行為整個依賴圖生成初始化程式碼的繁瑣工作。

為什麼這是Go Cloud的一部分?

Go Cloud的目標是通過為合適的雲服務提供慣用的Go API,使編寫行動式雲應用程式變得更加容易。 例如, blob.Bucket提供了一個儲存API,其中包含亞馬遜S3和谷歌雲端儲存(GCS)的實現; 使用blob.Bucket編寫的應用程式可以交換實現而無需更改其應用程式邏輯。 但是,初始化程式碼本質上是特定於提供者的,並且每個提供者具有不同的依賴集。

例如, 構建GCS blob.Bucket需要gcp.HTTPClient ,最終需要google.Credentials ,而為S3構建一個則需要aws.Config ,最終需要AWS憑據。 因此,更新應用程式以使用不同的blob.Bucket實現涉及到我們上面描述的依賴關係圖的那種繁瑣的更新。 Wire的驅動用例是為了方便交換Go Cloud可移植API的實現,但同時它也是依賴注入的通用工具。

這些工作不是已經做過了嗎?

確實有許多依賴注入框架。對Go來說, Uber的digFacebook的inject都使用反射來進行執行時依賴注入。 Wire的主要靈感來自Java的Dagger 2 ,並且使用程式碼生成而不是反射或服務定位器

我們認為這種方法有幾個優點:

  • 當依賴關係圖變得複雜時,執行時依賴注入很難跟蹤和除錯。 使用程式碼生成意味著在執行時執行的初始化程式碼是常規的,慣用的Go程式碼,易於理解和除錯。不會因為框架的各種奇技淫巧而變得生澀難懂。特別重要的是,忘記依賴項等問題會成為編譯時錯誤,而不是執行時錯誤。
  • 服務定位器不同,不需要費心編造名稱來註冊服務。 Wire使用Go語法中的型別將元件與其依賴項連線起來。
  • 更容易防止依賴項變得臃腫。 Wire生成的程式碼只會匯入您需要的依賴項,因此您的二進位制檔案將不會有未使用的匯入。 執行時依賴性注入器在執行之前無法識別未使用的依賴項。
  • Wire的依賴圖是靜態可知的,這為工具化和視覺化提供了可能。

它是如何工作的?

Wire有兩個基本概念:提供者和注射器。

提供者是普通的Go函式,它們根據它們的依賴關係“提供”值,這些值被簡單地描述為函式的引數。 以下是一些定義三個提供程式的示例程式碼:

 // NewUserStore與我們上面看到的功能相同; 它是UserStore的提供者,
 //依賴於*Config和*mysql.DB。
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}

 // NewDefaultConfig是*Config的提供者,沒有依賴。
func NewDefaultConfig() *Config {...}

 // NewDB是基於某些連線資訊的* mysql.DB的提供者。
func NewDB(info *ConnectionInfo) (*mysql.DB, error) {...}
複製程式碼

通常一起使用的ProviderSets可以分組到ProviderSets 。 例如,在建立*UserStore時使用預設的*Config是很常見的,因此我們可以在ProviderSet中對NewUserStoreNewDefaultConfig進行分組:

var UserStoreSet = wire.ProviderSet(NewUserStore, NewDefaultConfig)
複製程式碼

注入器是被生成的函式,它們按依賴所需的順序呼叫提供者。 編寫注入器的簽名,包括任何所需的輸入作為引數,並插入對wire.Build的呼叫, wire.Build包含構造最終結果所需的提供者或提供者集的列表:

func initUserStore() (*UserStore, error) {
     //我們將得到一個錯誤,因為NewDB需要一個*ConnectionInfo
     //我們沒有提供。
    wire.Build(UserStoreSet, NewDB)
    return nil, nil  // 這些返回值會被忽略。
}
複製程式碼

現在我們執行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  // 這些返回值會被忽略。
}
複製程式碼

現在go generate將使用生成的程式碼建立一個新檔案:

// File: wire_gen.go
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject

 func initUserStoreinfo ConnectionInfo)(* UserStoreerror{
     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使用體驗的任何意見! Go Cloud的開發是在GitHub上進行的,所以你可以提出一個問題來告訴我們什麼可能更好。 有關專案的更新和討論,請加入專案的郵件列表

感謝您抽出寶貴時間瞭解Go Cloud的Wire。 我們很高興與您合作,使Go成為構建可移植雲應用程式的開發人員的首選語言。

作者:Robert van Gent

原文:blog.golang.org/wire

相關文章