日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

Golang?官方依賴注入工具wire示例詳解_Golang

作者:ag9920 ? 更新時間: 2022-11-23 編程語言

依賴注入是什么

Dependency Injection is the idea that your components (usually structs in go) should receive their dependencies when being created.

在 Golang 中,構造一個結構體常見的有兩種方式:

  • 在結構體初始化過程中,構建它的依賴;
  • 將依賴作為構造器入參,傳入進來。

所謂依賴注入就是第二種思想。不夸張的說,依賴注入是保持我們的軟件系統松耦合,可維護的最重要的設計原則。

為什么?

因為當你的依賴通過入參傳入,意味著從本對象的角度,你不用去關心它的生成,只用關心它的能力。更具體來講,它能讓我們更加傾向于定義好接口,以接口方法來進行交互。而不是依賴一個具體的實現。

由此而來的另一個好處在于測試。由于依賴是傳入的,你的系統只管用它的能力,那么具體這個能力如何實現,其實是由上層來控制的。我們就可以很方便地進行 mock,調整各個場景下依賴的實現,來驗證我們的 SUT 的表現。

開源選型

Golang 社區中實現依賴注入的框架有很多,常用的主要是 google/wire, facebook/inject, uber/dig, uber/fx 等,我們這個專欄此前就介紹過 goioc/di,大家感興趣的話可以往前翻一下。

大體上看,分為兩個派系:

  • 代碼生成 codegen
  • 基于反射 reflect

其實不光是 DI 工具,針對 Golang 這種強類型,但泛型能力較弱的語言,包括 copier,orm 這類通用框架都會傾向于在這兩個路徑上二選一。

同樣的,DI 也存在這兩個排序,上面我們列舉的選項中,facebook/inject, uber/dig, uber/fx,以及我們此前介紹的 goioc/di 都采用了基于反射的解法。這樣的好處在于使用起來相對直接,不需要額外生成代碼。但劣勢也是相對的,失去了編譯器檢查的能力,如果注入有問題,只能在運行時報錯,啟動時會存在一些性能消耗。

google/wire 是 Google 官方提出的解決方案,也是業界目前最經典的基于 codegen 來解決依賴注入的開源庫。相較于反射這種在運行時搞事情的操作,wire 需要開發者提前使用代碼生成工具,觸發依賴注入代碼的生成,在編譯器干活。相對的,會稍微麻煩點,但語義更清晰,也消除了運行時的成本。

今天我們就來看看 wire 是怎么用的。

wire

Wire is a code generation tool that automates connecting components using dependency injection. Dependencies between components are represented in Wire as function parameters, encouraging explicit initialization instead of global variables. Because Wire operates without runtime state or reflection, code written to be used with Wire is useful even for hand-written initialization.

wire 在設計上受到了 Java’s?Dagger 2 的啟發。正如官方對它的定位,wire 是一個 Compile-time Dependency Injection for Go (編譯期依賴注入)的代碼生成工具。wire 非常的輕量級,只會幫助開發者進行按需初始化。

你甚至可以用手寫的初始化代碼來替換它,wire 作為一個代碼生成工具,僅僅是幫助我們減少注入依賴的繁瑣工作。

一個經典的 DI 函數簽名類似下面這樣:

// NewUserStore returns a UserStore that uses cfg and db as dependencies.
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}

我們需要生成一個 UserStore,所以需要從函數入參中,獲取 Config 配置,以及一個 MySQL 的 DB 連接。

思考一下,其實創建對象無非是兩種情況:

  • 沒有額外依賴,在當前場景下可以直接創建對象;
  • 存在外部依賴,我們需要先對外部依賴進行構建,然后作為參數傳進來,進而構建當前對象。

所以,要調用這個 NewUserStore,我們先構建兩個依賴。如果 cfg 和 db 都是第一種情況這種簡單對象,其實我們手寫就夠了。

但在生產環境大型應用中,依賴樹的構建可能是極其復雜的。A 依賴 B,B 依賴 C 和 D,C 又依賴 E,這個鏈路可能很長。這意味著如果手寫,你的初始化代碼會非常冗余,而且很可能要注意初始化順序。

而且有的依賴可能不僅僅在某一個父對象中使用,而是在多個對象中共用。這個過程是非常痛苦的。一句話:

In practice, making changes to initialization code in applications with large dependency graphs is tedious and slow.

那 wire 干的是一件什么事呢?

wire 希望幫助我們搞清楚,到底我要構建的這些對象,存在哪些依賴,如何一步步構建出來,保證每個對象都能得到它需要的依賴。你不需要考慮這些事情了。

如果要調整一個對象的依賴,我們直接把它的構造器從 wire 模板中增加或刪除,或者調整函數簽名即可,讓 wire 自己去搞清楚,怎么讓整個 dependency graph 完整。

wire 的設計中,需要開發者理解兩個概念:providers,injectors。下面我們分別來看看。

providers

Providers 就是我們常說的構造器,它們就是一些 Golang 函數,基于一些依賴參數(也可以沒有),來構造出來對象。我們經常用的 NewXXX() XXX 就是經典的 Provider,下面是三個例子:

// 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) {...}

實際上,我們可能會需要提供非常多 Provider,畢竟一個大型項目中涉及的依賴量級是很大的。所以 wire 提供了 ProviderSet 的概念,用來聚合一組 Provider。拿上面 UserStore 來舉例,我們可以這樣:

var UserStoreSet = wire.ProviderSet(NewUserStore, NewDefaultConfig)

injectors

injectors 也代表了一類函數,和 provider 提供構造器不同,它要做的事情在于實際去注入依賴。

什么?不是說好了 wire 幫我們搞么?怎么還要我們自己寫 injector ?

不要慌,的確是 wire 來做,但 wire 需要我們的幫助才能做到這一點。我們總得告訴 wire 我們想要啥樣的 injector 簽名吧?遇見錯誤返回不?要用哪些 provider?

要知道,provider 可不僅僅包括那些簡單的構造函數,有些對象構造的時候需要別的依賴作為參數,它們自己的構造器也是 provider。我們只有告訴 wire 有哪些 provider,它才能知道要給哪些對象進行構造。

所以,我們需要在這里做好兩件事:

  • 明確 injector 的函數簽名,確定好入參;
  • 調用 wire.Build,傳入一系列 provider(或者 providerSet),wire 將會以此來構造最終結果。
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.
}

看看示例,發現了么?

除了這兩步我們什么都不用干,甚至直接 return 了兩個 nil。不要慌,這個函數不是最后要用的,wire 會忽略它的返回值,只需要簽名,以及 wire.Build 這個信息。最終我們使用的 injector 并不是自己寫的這個。

好,下來操練一下,首先我們安裝一下 wire 工具:

go install github.com/google/wire/cmd/wire@latest

安裝結束后,直接在當前目錄運行 wire 即可。輸出如下信息:

$ wire
wire.go:2:10: inject initUserStore: no provider found for ConnectionInfo (required by provider of *mysql.DB)
wire: generate failed

這里信息很明確,上面我們的 func NewDB(info *ConnectionInfo) (*mysql.DB, error) {...} 要求傳入 ConnectionInfo,但是我們調用 wire.Build 里面沒有對應的 Provider,所以無法生成。

這里我們有兩種方案:

  • 加上 ConnectionInfo 依賴作為參數,表明我們這個構造器,就得顯式傳入;
  • 加上 Provider。

我們試試第一種:

func initUserStore(info ConnectionInfo) (*UserStore, error) {
    wire.Build(UserStoreSet, NewDB)
    return nil, nil  // These return values are ignored.
}

只是加了個入參,看看 wire 能不能識別出來。再次觸發命令,會發現目錄下多了個 wire_gen.go

// 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 全都搞定了。這里的簽名和我們預期的也一樣。

這里也能看到,wire 其實非常輕量級,只是把原本需要開發者手寫的構建流程,自動生成了。依賴越多,它的作用就越大。

有了生成的代碼,我們就可以繼續自己的初始化流程,wire 就是個縮減大家人工的小幫手。

類型區分

wire不允許不同的組件擁有相同的類型。官方認為這是設計上的缺陷。我們可以通過類型別名來將組件的類型進行區分。例如服務會同時操作兩個Redis,redisA, redisB,不要用這樣,wire 無法推導出依賴關系:

func NewRedisA() *goredis.Client {...}
func NewRedisB() *goredis.Client {...}

建議用:

type RedicCliA *goredis.Client
type RedicCliB *goredis.Client
func NewRedisA() RedicCliA {...}
func NewRedisB() RedicCliB {...}

總結

這一篇我們只是從理念和基礎用法上帶大家初步理解 wire 的定位,更多用法可以參照官方的 tutorial

使用 wire 可以把性能消耗收斂在編譯期,但隨之而來的代價就是需要編寫wire.go文件,生成wire_gen.go,且需要為所有struct編寫構造函數,而且需要學習wire.go的寫法。

原文鏈接:https://juejin.cn/post/7152874431692898312

欄目分類
最近更新