golang依賴注入工具wire指南

2021-02-21 NodeJs之路
wire與依賴注入

Wire 是一個的Golang依賴注入工具,通過自動生成代碼的方式在編譯期完成依賴注入,Java體系中最出名的Spring框架採用運行時注入,個人認為這是wire和其他依賴注入最大的不同之處。

依賴注入(Dependency Injection)也稱作控制反轉(Inversion of Control),個人給控制反轉下的定義如下:

當前對象需要的依賴對象由外部提供(通常是IoC容器),外部負責依賴對象的構造等操作,當前對象只負責調用,而不關心依賴對象的構造。即依賴對象的控制權交給了IoC容器。

下面給出一個控制反轉的示例,比如我們通過配置去創建一個資料庫連接:

// 連接配置
type DatabaseConfig struct {
    Dsn string 
}

func NewDB(config *DatabaseConfig)(*sql.DB, error) {
    db,err := sql.Open("mysql", config.Dsn)
    if err != nil {
        return nil, err
    }
    // ...
}

fun NewConfig()(*DatabaseConfig,error) {
    // 讀取配置文件
    fp, err := os.Open("config.json")
    if err != nil {
        return nil,err
    }
    defer fp.Close()
    // 解析為Json
    var config DatabaseConfig
    if err:=json.NewDecoder(fp).Decode(&config);err!=nil {
        return nil,err
    }
    return &config, nil
}

func InitDatabase() {
    cfg, err:=NewConfig()
    if err!=nil {
        log.Fatal(err)
    }
    db,err:=NewDB(cfg)
    if err!=nil {
        log.Fatail(err)
    }
    // db對象構造完畢
}

資料庫配置怎麼來的,NewDB方法並不關心(示例代碼採用的是NewConfig提供的JSON配置對象),NewDB只負責創建DB對象並返回,和配置方式並沒有耦合,所以即使換成配置中心或者其他方式來提供配置,NewDB代碼也無需更改,這就是控制反轉的魔力!

來看一個反面例子,也就是控制正轉:

當前對象需要的依賴由自己創建,即依賴對象的控制權在當前對象自己手裡。

type DatabaseConfig struct {
    Dsn string 
}

func NewDB()(*sql.DB, error) {
    // 讀取配置文件
    fp, err := os.Open("config.json")
    if err != nil {
        return nil,err
    }
    defer fp.Close()
    // 解析為Json
    var config DatabaseConfig
    if err:=json.NewDecoder(fp).Decode(&config);err!=nil {
        return nil,err
    }
    // 初始化資料庫連接
    db,err = sql.Open("mysql", config.Dsn)
    if err != nil {
        return
    }
    // ...
}

在控制正轉模式下,NewDB方法需要自己實現配置對象的創建工作,在示例中需要讀取Json配置文件,這是強耦合的代碼,一旦配置文件的格式不是Json,NewDB方法將返回錯誤。

依賴注入固然好用,但是像剛才的例子中去手動管理依賴關係是相當複雜也是相當痛苦的一件事,因此在接下來的內容中會重點介紹golang的依賴注入工具——wire。

上手使用

通過go get github.com/google/wire/cmd/wire安裝好wire命令行工具即可。

在正式開始之前需要介紹一下wire中的兩個概念:Provider和Injector:

Provider:負責創建對象的方法,比如上文中控制反轉示例的NewDB(提供DB對象)和NewConfig(提供DatabaseConfig對象)方法。Injector:負責根據對象的依賴,依次構造依賴對象,最終構造目的對象的方法,比如上文中控制反轉示例的InitDatabase方法。

現在我們通過wire來實現一個簡單的項目。項目結構如下:

|--cmd
 |--main.go
 |--wire.go
|--config
 |--app.json
|--internal
 |--config
  |--config.go
 |--db
  |--db.go

config/app.json

{
  "database": {
    "dsn": "root:root@tcp(localhost:3306)/test"
  }
}

internal/config/config.go

package config

import (
 "encoding/json"
 "github.com/google/wire"
 "os"
)

var Provider = wire.NewSet(New) // 將New方法聲明為Provider,表示New方法可以創建一個被別人依賴的對象,也就是Config對象

type Config struct {
 Database database `json:"database"`
}

type database struct {
 Dsn string `json:"dsn"`
}

func New() (*Config, error) {
 fp, err := os.Open("config/app.json")
 if err != nil {
  return nil, err
 }
 defer fp.Close()
 var cfg Config
 if err := json.NewDecoder(fp).Decode(&cfg); err != nil {
  return nil, err
 }
 return &cfg, nil
}

internal/db/db.go

package db

import (
 "database/sql"
 _ "github.com/go-sql-driver/mysql"
 "github.com/google/wire"
 "wire-example2/internal/config"
)

var Provider = wire.NewSet(New) // 同理

func New(cfg *config.Config) (db *sql.DB, err error) {
 db, err = sql.Open("mysql", cfg.Database.Dsn)
 if err != nil {
  return
 }
 if err = db.Ping(); err != nil {
  return
 }
 return db, nil
}

cmd/main.go

package main

import (
 "database/sql"
 "log"
)

type App struct { // 最終需要的對象
 db *sql.DB
}

func NewApp(db *sql.DB) *App {
 return &App{db: db}
}

func main() {
 app, err := InitApp() // 使用wire生成的injector方法獲取app對象
 if err != nil {
  log.Fatal(err)
 }
 var version string
 row := app.db.QueryRow("SELECT VERSION()")
 if err := row.Scan(&version); err != nil {
  log.Fatal(err)
 }
 log.Println(version)
}

cmd/wire.go

重點文件,也就是實現Injector的核心所在:

// +build wireinject

package main

import (
 "github.com/google/wire"
 "wire-example2/internal/config"
 "wire-example2/internal/db"
)

func InitApp() (*App, error) {
 panic(wire.Build(config.Provider, db.Provider, NewApp)) // 調用wire.Build方法傳入所有的依賴對象以及構建最終對象的函數得到目標對象
}

文件編寫完畢,進入cmd目錄執行wire命令會得到以下輸出:

C:\Users\Administrator\GolandProjects\wire-example2\cmd>wire
wire: wire-example2/cmd: wrote C:\Users\Administrator\GolandProjects\wire-example2\cmd\wire_gen.go

表明成功生成wire_gen.go文件,文件內容如下:

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//+build !wireinject

package main

import (
 "wire-example2/internal/config"
 "wire-example2/internal/db"
)

// Injectors from wire.go:

func InitApp() (*App, error) {
 configConfig, err := config.New()
 if err != nil {
  return nil, err
 }
 sqlDB, err := db.New(configConfig)
 if err != nil {
  return nil, err
 }
 app := NewApp(sqlDB)
 return app, nil
}

可以看到生成App對象的代碼已經自動生成了。

Provider說明

通過NewSet方法將本包內創建對象的方法聲明為Provider以供其他對象使用。NewSet可以接收多個參數,比如我們db包內可以創建Mysql和Redis連接對象,則可以如下聲明:

var Provider = wire.NewSet(NewDB, NewRedis)

func NewDB(config *Config)(*sql.DB,error) { // 創建資料庫對象
    
}

func NewRedis(config *Config)(*redis.Client,error) { // 創建Redis對象
}

wire.go文件說明

wire.go文件需要放在創建目標對象的地方,比如我們Config和DB對象最終是為App服務的,因此wire.go文件需要放在App所在的包內。

wire.go文件名不是固定的,不過大家習慣叫這個文件名。

wire.go的第一行// +build wireinject是必須的,含義如下:

只有添加了名稱為"wireinject"的build tag,本文件才會編譯,而我們go build main.go的時候通常不會加。因此,該文件不會參與最終編譯。

wire.Build(config.Provider, db.Provider, NewApp)通過傳入config以及db對象來創建最終需要的App對象

wire_gen.go文件說明

該文件由wire自動生成,無需手工編輯!!!

//+build !wireinject標籤和wire.go文件的標籤相對應,含義如下:

編譯時只有未添加"wireinject"的build tag,本文件才參與編譯。

因此,任意時刻下,wire.go和wire_gen.go只會有一個參與編譯。

高級玩法 cleanup函數

在創建依賴資源時,如果由某個資源創建失敗,那麼其他資源需要關閉的情況下,可以使用cleanup函數來關閉資源。比如咱們給db.New方法返回一個cleanup函數來關閉資料庫連接,相關代碼修改如下(未列出的代碼不修改):

internal/db/db.go

func New(cfg *config.Config) (db *sql.DB, cleanup func(), err error) { // 聲明第二個返回值
 db, err = sql.Open("mysql", cfg.Database.Dsn)
 if err != nil {
  return
 }
 if err = db.Ping(); err != nil {
  return
 }
 cleanup = func() { // cleanup函數中關閉資料庫連接
  db.Close()
 }
 return db, cleanup, nil
}

cmd/wire.go

func InitApp() (*App, func(), error) { // 聲明第二個返回值
 panic(wire.Build(config.Provider, db.Provider, NewApp))
}

cmd/main.go

func main() {
 app, cleanup, err := InitApp() // 添加第二個參數
 if err != nil {
  log.Fatal(err)
 }
 defer cleanup() // 延遲調用cleanup關閉資源
 var version string
 row := app.db.QueryRow("SELECT VERSION()")
 if err := row.Scan(&version); err != nil {
  log.Fatal(err)
 }
 log.Println(version)
}

重新在cmd目錄執行wire命令,生成的wire_gen.go如下:

func InitApp() (*App, func(), error) {
 configConfig, err := config.New()
 if err != nil {
  return nil, nil, err
 }
 sqlDB, cleanup, err := db.New(configConfig)
 if err != nil {
  return nil, nil, err
 }
 app := NewApp(sqlDB)
 return app, func() { // 返回了清理函數
  cleanup()
 }, nil
}

接口綁定

在面向接口編程中,代碼依賴的往往是接口,而不是具體的struct,此時依賴注入相關代碼需要做一點小小的修改,繼續剛才的例子,示例修改如下:

新增internal/db/dao.go

package db

import "database/sql"

type Dao interface { // 接口聲明
 Version() (string, error)
}

type dao struct { // 默認實現
 db *sql.DB
}

func (d dao) Version() (string, error) {
 var version string
 row := d.db.QueryRow("SELECT VERSION()")
 if err := row.Scan(&version); err != nil {
  return "", err
 }
 return version, nil
}

func NewDao(db *sql.DB) *dao { // 生成dao對象的方法
 return &dao{db: db}
}

internal/db/db.go也需要修改Provider,增加NewDao聲明:

var Provider = wire.NewSet(New, NewDao)

cmd/main.go文件修改:

package main

import (
 "log"
 "wire-example2/internal/db"
)

type App struct {
 dao db.Dao // 依賴Dao接口
}

func NewApp(dao db.Dao) *App { // 依賴Dao接口
 return &App{dao: dao}
}

func main() {
 app, cleanup, err := InitApp()
 if err != nil {
  log.Fatal(err)
 }
 defer cleanup()
 version, err := app.dao.Version() // 調用Dao接口方法
 if err != nil {
  log.Fatal(err)
 }
 log.Println(version)
}

進入cmd目錄執行wire命令,此時會出現報錯:

C:\Users\Administrator\GolandProjects\wire-example2\cmd>wire
wire: C:\Users\Administrator\GolandProjects\wire-example2\cmd\wire.go:11:1: inject InitApp: no provider found for wire-example2/internal/db.Dao
        needed by *wire-example2/cmd.App in provider "NewApp" (C:\Users\Administrator\GolandProjects\wire-example2\cmd\main.go:12:6)
wire: wire-example2/cmd: generate failed
wire: at least one generate failure

wire提示inject InitApp: no provider found for wire-example2/internal/db.Dao,也就是沒找到能提供db.Dao對象的Provider,咱們不是提供了默認的db.dao實現也註冊了Provider嗎?這也是go的OOP設計奇特之處。

咱們修改一下internal/db/db.go的Provider聲明,增加db.*dao和db.Dao的接口綁定關係:

var Provider = wire.NewSet(New, NewDao, wire.Bind(new(Dao), new(*dao)))

wire.Bind()方法第一個參數為interface{},第二個參數為實現。

此時再執行wire命令就可以成功了!

結尾

wire工具還有很多玩法,但是就筆者個人工作經驗而言,掌握本文介紹到的知識已經能夠勝任絕大部分場景了!

相關焦點

  • Go技術日報(2021-02-22)——Go 工程化(三) 依賴注入框架 wire
    三、HHFCodeRv四、MoeLove五、奇伢雲存儲六、網管叨 bi 叨七、mohuishouGo 工程化(三) 依賴注入框架wire - Mohuishou[1]來源: https://studygolang.com/go/godailyGOCN 每日新聞--2021-02-22 1.一文完全掌握 Go math/rand [2]2.詳解 Go 語言調度循環源碼實現 [Go 15.7 源碼] [3]
  • 不到30行代碼實現 golang 依賴注入
    【導讀】golang 做依賴注入有什麼思路和實現方案?本文中作者用例子做了介紹。使用標準庫實現,無額外依賴依賴注入的優勢用java的人對於spring框架一定不會陌生,spring核心就是一個IoC(控制反轉/依賴注入)容器,帶來一個很大的優勢是解耦
  • Golang 簡潔架構實戰
    type articleService struct {    articleRepo repo.IArticleRepo}// NewArticleService will create new an articleUsecase object representation of domain.ArticleUsecase interfacefunc NewArticleService
  • golang 依賴管理之 mod
    ,內置 go get 命令,可以直接獲取對應的依賴,非常方便,但是有一個巨大的缺陷,沒有版本的維護和管理,而版本不一致可能會導致各種兼容性問題,因此出現了很多第三方依賴管理工具,dep 和 glide 就是其中的佼佼者,到了 go 1.11 官方終於推出了自己的依賴管理工具 mod,並內置在 go 版本中,go mod 使用簡單,功能強大,並且能自動兼容大多數之前的第三方工具,大量優秀的開源庫都已經切換到了
  • seata-golang 接入指南
    seata 的 AT 模式將全局鎖放在 transaction coordinator 也就是事務協調器上,依賴於具體鎖接口的存儲實現方式可以是 file/db/redis 等,而不是資料庫鎖,每個分支事務提交時立即釋放資料庫鎖,這樣對資料庫的壓力也就減小了,變相得提升了資料庫的性能。seata AT 模式和 TCC 模式的原理見:[Seata 是什麼?]
  • Go 包依賴管理工具 —— govendor
    govendor 是一個基於 vendor 機制實現的 Go 包依賴管理命令行工具。
  • Android Hilt依賴注入框架的使用
    中國移動家庭運營中心融合通信 閆同學一、什麼是依賴注入?依賴注入是一個聽起來很「高大上」的概念,但是其實很簡單。本來我要接受各種參數自己構造一個對象,現在只接受一個已經實例化的對象直接作為參數。所謂依賴注入,其實就是之前在類的內部自己構造成員變量,現在放到了外部構造成員變量。而Hilt,其實就是Android基於Dragger開發的一套Android上的依賴注入框架而已。
  • 開發(三) 依賴注入
    簡單來講,它是使用大型的類型解析容器來解析類的依賴的一種方式。幾乎每一種現代化的程式語言都有很多的與之對應的依賴注入框架。我們重點關注C#語言,它也有很多的依賴注入框架,像是Ninject,AutoFac,Unity( Microsoft),還有很多其他的。
  • XUnit 依賴注入
    XUnit 依賴注入
  • 達觀數據:Angular 6+依賴注入使用指南:providedIn與providers對比
    依賴注入回顧(可選)使用舊語法進行依賴注入—— providers: []使用新語法進行依賴注入—— providedIn: 'root' | SomeModuleprovidedIn 的使用場景在項目中如何使用新語法的最佳實踐總結依賴注入讓我們快速回顧一下依賴注入是什麼,如果感覺簡單,你可以跳過這一小節
  • 依賴注入與對象間的關係
    依賴注入 vs 創建對象有不少地方這樣描述:「依賴注入改變了使用對象前先創建的傳統方式,而是從外部注入依賴的對象」。應該說這是一種合理的設計,但如果接本段開頭的話說「依賴注入改變了人使用汽車之前先創建的傳統方式」您會不會覺得別捏呢?
  • 依賴注入和控制反轉
    概念 IoC——Inversion of Control  控制反轉 DI——Dependency Injection   依賴注入要想理解上面兩個概念
  • 理解JavaScript中的依賴注入
    隨著我們項目的增長,我們需要依賴的模塊變得越來越多,這個時候,如何有效的組織這些模塊就成了一個非常重要的問題。依賴注入解決的正是如何有效組織代碼依賴模塊的問題。你可能在一些框架或者庫種聽說過「依賴注入」這個詞,比如說著名的前端框架AngularJS,依賴注入就是其中一個非常重要的特性。
  • 工具 | 常用工具鏡像網站又更新了(Golang、Chrome、VsCode、Android Studio等) #5
    最近Golang1.8.5和1.9.2、Android Studio 3.0正式版、Chrome以及VSCode等都更新了新版,為了讓大家更好的下載這些新版本的工具軟體,特地對這些工具等進行了一次更新。Android StudioAndroid Studio3.0正式版發布了,支持Kotlin,可以下載使用了。
  • .net core 依賴注入: 什麼是控制反轉,什麼是依賴注入,什麼是容器, autofac 簡單使用
    綜述ASP.NET Core  支持依賴注入, 也推薦使用依賴注入. 主要作用是用來降低代碼之間的耦合度.
  • WebAPI使用依賴注入
    (點擊上方藍字,可快速關注我們)來源:神牛步行3cnblogs.com/wangrudong003/p/6253486.html本篇將要和大家分享的是webapi中如何使用依賴注入,依賴注入這個東西在接口中常用,實際工作中也用的比較頻繁,因此這裡分享兩種在api中依賴注入的方式Ninject和Unity。
  • Golang 入門 : 配置代理
    由於一些客觀原因的存在,我們開發 Golang 項目的過程總會碰到無法下載某些依賴包的問題。
  • 工具 | 常用工具鏡像網站又更新了(Golang、VsCode、Android Studio等) #4
    最近Golang,Android NDK、Chrome以及VSCode等都更新了新版,為了讓大家更好的下載這些新版本的工具軟體,特地對這些工具等進行了一次更新。Android StudioAndroid Studio最新版本為3.0.0.0,可以下載使用了。