依賴注入工具程式碼生成器 wire

jacobxy發表於2021-03-19

Golang | wire 庫

推薦編輯:不落凡塵

簡介

wire 是一個程式碼生成工具,它通過自動生成程式碼的方式完成依賴注入。

應用場景

wire 作為依賴注入的程式碼生成工具,非常適合複雜物件的建立。而在大型專案中,擁有一個合適的依賴注入的框架將使得專案的開發與維護十分便捷。

Wire 核心概念

wire 中最核心的兩個概念就是 Injector 和 Provider。

Provider : 生成元件的普通方法。這些方法接收所需依賴作為引數,建立元件並將其返回

Injector : 代表了我們最終要生成的構建函式的函式簽名,返回值代表了構建的目標,在最後生成的程式碼中,此函式簽名會完整的保留下來。

安裝

go get github.com/google/wire/cmd/wire

程式碼生成

命令列在指定目錄下執行 wire命令即可。

示例學習

官方示例

成員介紹

func NewSet(...interface{}) ProviderSet
func Build(...interface{}) string
func Bind(iface, to interface{}) Binding
func Struct(structType interface{}, fieldNames ...string) StructProvider
func FieldsOf(structType interface{}, fieldNames ...string) StructFields
func Value(interface{}) ProvidedValue
func InterfaceValue(typ interface{}, x interface{}) ProvidedValue

基礎程式碼

main.go


package main


type Leaf struct {
    Name string
}

type Branch struct{
    L Leaf
}

type Root struct {
    B Branch
}

func NewLeaf(name string) Leaf {return Leaf{Name:name}}
func NewBranch(l Leaf) Branch {return Branch{L:l}}
func NewRoot(b Branch) Root {return Root{B:b}}

wire.go

// +build wireinject

// The build tag makes sure the stub is not built in the final build.

package main

import (
    "github.com/google/wire"
)

func InitRoot(name string) Root {
    wire.Build(NewLeaf,NewBranch,NewRoot)
    return Root{}
}

wire_gen.go

// Code generated by Wire. DO NOT EDIT.

//go:generate wire
//+build !wireinject

package main

// Injectors from wire.go:

func InitRoot(name string) Root {
    leaf := NewLeaf(name)
    branch := NewBranch(leaf)
    root := NewRoot(branch)
    return root
}

這裡我們可以看到程式碼的生成是根據 wire.Build 引數的輸入與輸出型別來決定的。

wire.Build 的引數是 Provider 的不定長列表。

wire 包成員的作用

wire 的成員每一個都是為了 Provider 服務的,他們各自有適用的場景。

NewSet

NewSet 的作用是為了防止 Provider 過多導致混亂,它把一組業務相關的 Provider 放在一起組織成 ProviderSet。

wire.go 可以寫成

var NewBranchSet = wire.NewSet(NewLeaf,NewBranch)
func InitRoot(name string) Root {
    wire.Build(NewBranchSet,NewRoot)
    return Root{}
}

值得注意的事,NewSet 可以寫在原結構體所在的檔案中,以方便切換和維護。

Bind

Bind 函式的作用是為了讓介面型別參與 wire 的構建過程。wire 的構建依靠的是引數的型別來組織程式碼,所以介面型別天然是不支援的。Bind 函式通過將介面型別和實現型別繫結,來達到依賴注入的目的。

type Fooer interface{
    HelloWorld() 
}
type Foo struct{}
func (f Foo)HelloWorld(){}

var bind = wire.Bind(new(Fooer),new(Foo))

示例

這樣將 bind 傳入 NewSet 或 Build 中就可以將 Fooer 介面和 Foo 型別繫結。

這裡需要特別注意,如果是 *Foo 實現了 Fooer 介面,需要將最後的 new(Foo) 改成 new(*Foo)

Struct

Struct 函式用於簡化結構體的 Provider,當結構體的 Provider 僅僅是欄位賦值時可以使用這個函式。


//當Leaf中成員變數很多時,或者只需要部分初始化時,建構函式會變得很複雜
func NewLeaf(name string) Leaf {return Leaf{Name:name}}

//等價寫法
//部分欄位初始化
wire.Struct(new(Leaf),"Name")
//全欄位初始化
wire.Struct(new(Leaf),"*")

這裡的 NewLeaf 函式可以被下面的部分欄位初始化函式替代。

Struct 函式可以作為 Provider 出現在 Build 或 NewSet 的引數中。

FieldsOf

FieldsOf 函式可以將結構體中的對應欄位作為 Provider,供 wire 使用。 在上面的程式碼基礎上,我們做如下的等價

//獲得Leaf中Name欄位的Provider
func NewName(l Leaf) string {return l.Name}

//等價寫法
//FieldsOf的方式獲得結構體內的欄位
wire.FieldsOf(new(Leaf),"Name")

示例

這裡的程式碼是等價的,但是卻不能和上面的程式碼共存,原因稍後會解釋。

Value

Value 函式為基本型別的屬性繫結具體值,在基於需求的基礎上簡化程式碼。


func NewLeaf()Leaf{
    return Leaf{
        Name:"leaf",
    }
}

//等價寫法
wire.Value(Leaf{Name:"leaf"})

以上兩個函式在作為 Provider 上也是等價的,可以出現在 Build 或 NewSet 中。

InterfaceValue

InterfaceValue 作用與 Value 函式類似,只是 InterfaceValue 函式是為介面型別繫結具體值。

wire.InterfaceValue(new(io.Reader),os.Stdin)

比較少用到,這裡就不細講了。

返回值的特殊情況

返回值 error

wire 是支援返回物件的同時攜帶 error 的。對於 error 型別的返回值,wire 也能很好的處理。

//main.go
func NewLeaf(name string) (Leaf, error) { return Leaf{Name: name}, nil }

//wire.go
func InitRoot(name string) (Root, error) {
    ...
}

//wire_gen.go
func InitRoot(name string) (Root, error) {
    leaf, err := NewLeaf(name)
    if err != nil {
        return Root{}, err 
    }   
    branch := NewBranch(leaf)
    root := NewRoot(branch)
    return root, nil 
}

示例

可以看到當 Provider 中出現 error 的返回值時,Injector 函式的返回值中也必須攜帶 error 的返回值

清理函式 CleanUp

清理通常出現在有檔案物件,socket 物件參與的構建函式中,無論是出錯後的資源關閉,還是作為正常獲得物件後的解構函式都是有必要的。

清理函式通常作為第二返回值,引數型別為 func(),即為無引數無返回值的函式物件。跟 error 一樣,當 Provider 中的任何一個擁有清理函式,Injector 的函式簽名返回值中也必須包含該函式型別。

//main.go
func NewLeaf(name string) (Leaf, func()) {
    r := Leaf{Name: name}
    return r, func() { r.Name = "" }
}
func NewBranch(l Leaf) (Branch, func()) { return Branch{L: l}, func() {} }


//wire.go
func InitRoot(name string) (Root, func()) {...}

//wire_gen.go
func InitRoot(name string) (Root, func()) {
    leaf, cleanup := NewLeaf(name)
    branch, cleanup2 := NewBranch(leaf)
    root := NewRoot(branch)
    return root, func() {
        cleanup2()
        cleanup()
    }   
}

示例

就這樣名為 cleanup 的清理函式就隨著 InitRoot 返回了。當有多個 Provider 有 cleanup 的時候,wire 會自動把 cleanup 加入到最後的返回函式中。

常見問題

型別重複

基礎型別

基礎型別是構建結構體的基礎,其作為引數建立結構體是十分常見的,引數型別重複更是不可避免的。wire 通過 Go 語言語法中的"type A B"的方法來解決詞類問題。

//wire.go
type Account string
func InitRoot(name string, account Account) (Root, func()) {...}

出現在 wire.go 中的"type A B" 會自動複製到 wire_gen.go 中

示例

個人觀點 wire 著眼於複雜物件的構建,因此基礎型別的屬性賦值推薦使用結構體本身的 Set 操作完成。

物件型別重複

每一個 Provider 都是一個元件的生成方法,如果有兩個 Provider 生成同一類元件,那麼在構建過程中就會產生衝突,這裡需要特別注意,保證元件的型別唯一性。

迴圈構建

迴圈構建指的是多個 Provider 相互提供引數和返回值形成一個閉環。 當 wire 檢查構建的流程含有閉環構建的時候,就會報錯。

type Root struct{
    B Branch
}
type Branch struct {
    L Leaf
}
type Leaf struct {
    R Root
}
func NewLeaf(r Root) Leaf {return Leaf{R:r}}
func NewBranch(l Leaf) Branch {return Branch{L:l}}
func NewRoot(b Branch) Root {return Root{B:b}}

...
wire.Build(NewLeaf,NewRranch,NewRoot) //錯誤 cycle for XXX
...

示例

小結

wire 是一個強大的工具,它在不執行 Go 程式的基礎上,藉助於特定檔案 ("//+build wireinject") 的解析,自動生成物件的建構函式程式碼。

Go 語言工程化的過程中,涉及到諸多物件的包級別歸類,wire 可以很好的協助我們完成複雜物件的構建過程。

還想了解更多嗎?

更多請檢視: https://github.com/google/wire

歡迎加入我們 GOLANG 中國社群:https://gocn.vip

更多原創文章乾貨分享,請關注公眾號
  • 依賴注入工具程式碼生成器 wire
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章