Go 官方依賴注入工具wire

guyan0319發表於2022-12-17

wire是Go官方推出的一款類似於Spring依賴注入工具。有別於以往的依賴注入工具facebookgo/inject、uber-go/dig等,採用反射實現。wire採用透過程式碼描述物件之間的依賴關係,然後自動生成程式碼在編譯期實現依賴注入的工具
原始碼:https://github.com/google/wire

什麼是依賴注入

說到依賴注入(Dependency Injection,縮寫DI),不得不提控制反轉(Inversion of Control,縮寫為IoC)。IoC是一種設計思想,核心作用是降低程式碼耦合度。
傳統系統應用是在類內部主動引用物件,從而導致類與類之間高度耦合,不利於維護,而有了IoC容器後,把建立和查詢物件工作交給容器,由容器動態的將某個依賴關係注入物件中,控制權由呼叫者應用程式碼轉移到IoC容器,控制權發生了反轉,從而實現物件間解耦。依賴注入是實現IoC解決依賴問題的設計模式。

舉例

type TestA struct {
    B *TestB//依賴

}
type TestB struct {

}

func NewA() *TestA {
    return &TestA{
        B:new(TestB),
    }
}

上面程式碼,TestA依賴TestB,這樣以後加入需要對TestB修改時,還需要對TestA做修改。這樣做有以下幾個問題:
程式碼耦合性高不利於維護這種依賴關係
不利於功能複用,TestA無法複用示例好的TestB
不方便單元測試

為此我們可以對程式碼以依賴注入方式修改

type TestA struct {
    B *TestB

}
type TestB struct {

}

func NewA(b *TestB) *TestA {
    return &TestA{
        B:b,
    }
}

這樣,我們將初始化後的TestB注入到NewA中了,解耦了部分依賴。
以上的依賴注入方式,在程式碼少,系統不復雜時實現起來沒問題,當系統龐大到一定程式時就力不從心了。怎麼解決呢?這裡就需要著重介紹的wire依賴注入工具了。

wire 使用方法

安裝

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

並確保將$GOPATH/bin其新增到您的$PATH.

基本

Wire 有兩個核心概念:providers 和 injectors。
providers 示例
在foobarbaz目錄下建立檔案foobarbaz.go,內容如下

package foobarbaz

import (
    "context"
    "errors"
)

type Foo struct {
    X int
}

// ProvideFoo returns a Foo.
func ProvideFoo() Foo {
    return Foo{X: 42}
}

type Bar struct {
    X int
}

// ProvideBar returns a Bar: a negative Foo.
func ProvideBar(foo Foo) Bar {
    return Bar{X: -foo.X}
}

type Baz struct {
    X int
}

// ProvideBaz returns a value if Bar is not zero.
func ProvideBaz(ctx context.Context, bar Bar) (Baz, error) {
    if bar.X == 0 {
        return Baz{}, errors.New("cannot provide baz when bar is zero")
    }
    return Baz{X: bar.X}, nil
}

injectors:建立wire.go檔案(檔名可以不是wire,但一般是這個)
示例

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

package main

import (
    "context"
    "demo/foobarbaz"
    "github.com/google/wire"
)

func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
    wire.Build(foobarbaz.ProvideFoo,foobarbaz.ProvideBar,foobarbaz.ProvideBaz)
    return foobarbaz.Baz{}, nil
}

注意:需要在檔案頭部增加構建約束://+build wireinject
執行wire自動生成依賴程式碼,可以直接用wire或者用wire gen wire.go來生成wire_gen.go檔案。
生成程式碼如下

// Code generated by Wire. DO NOT EDIT.

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

package main

import (
    "context"
    "demo/foobarbaz"
)

// Injectors from wire.go:

func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
    foo := foobarbaz.ProvideFoo()
    bar := foobarbaz.ProvideBar(foo)
    baz, err := foobarbaz.ProvideBaz(ctx, bar)
    if err != nil {
        return foobarbaz.Baz{}, err
    }
    return baz, nil
}

透過以上程式碼,可以看到自動生成的程式碼包含了error處理,跟手動寫的程式碼幾乎一樣。
wire.go檔案頭部//+build wireinject,+build 其實是 Go 語言的一個特性,確保在go build編譯時不處理此檔案。
wire_gen.go檔案頭部// +build !wireinject,其中的!wireinject是來告訴wire命令不處理此檔案。

高階特性

NewSet

NewSet 一般應用在初始化物件比較多的情況下,減少 Injector 裡面的資訊。

var SuperSet = wire.NewSet(ProvideFoo, ProvideBar, ProvideBaz)

wire.Build(SuperSet)

Struct

除了函式可以作為Provider外,stuct也可以作為Provider。對於生成的結構型別S,wire.Struct同時提供S和*S。
示例

type Foo int
type Bar int

func ProvideFoo() Foo {/* ... */}

func ProvideBar() Bar {/* ... */}

type FooBar struct {
    MyFoo Foo
    MyBar Bar
}

var Set = wire.NewSet(
    ProvideFoo,
    ProvideBar,
    wire.Struct(new(FooBar), "MyFoo", "MyBar"))

生成的注入器FooBar如下所示:

func injectFooBar() FooBar {
    foo := ProvideFoo()
    bar := ProvideBar()
    fooBar := FooBar{
        MyFoo: foo,
        MyBar: bar,
    }
    return fooBar
}

第一個引數wire.Struct是指向所需結構型別的指標,後續引數是要注入的欄位的名稱。一個特殊的字串""可以用作告訴注入器注入所有欄位的快捷方式。所以wire.Struct(new(FooBar), "")產生與上面相同的結果。
對於上面的示例,您可以"MyFoo"透過更改為 Set來指定僅注入:

var Set = wire.NewSet(
    ProvideFoo,
    wire.Struct(new(FooBar), "MyFoo"))

那麼生成的注入器FooBar將如下所示:

func injectFooBar() FooBar {
    foo := ProvideFoo()
    fooBar := FooBar{
        MyFoo: foo,
    }
    return fooBar
}

如果注入器返回 a*FooBar而不是 a FooBar,生成的注入器將如下所示:

func injectFooBar() *FooBar {
    foo := ProvideFoo()
    fooBar := &FooBar{
        MyFoo: foo,
    }
    return fooBar
}

有時防止某些欄位被注入器填充是很有用的,尤其是在傳遞*給wire.Struct. 您可以標記一個欄位, wire:"-"讓 Wire 忽略這些欄位。例如:

type Foo struct {
    mu sync.Mutex `wire:"-"`
    Bar Bar
}

當您使用 提供Foo型別時wire.Struct(new(Foo), "*"),Wire 將自動省略該mu欄位。此外,像在wire.Struct(new(Foo), "mu").

Bind

Bind 函式的作用是為了讓介面型別的依賴參與 Wire 的構建。Wire 的構建依靠引數型別,介面型別是不支援的。Bind 函式透過將介面型別和實現型別繫結,來達到依賴注入的目的。

type Foo struct {
    X int
}

func injectFoo() Foo {
    wire.Build(wire.Value(Foo{X: 42}))
    return Foo{}
}

CleanUp

如果提供者建立了一個需要清理的值(例如關閉檔案),那麼它可以返回一個閉包來清理資源。如果稍後在注入器實現中呼叫的提供者返回錯誤,注入器將使用它向呼叫者返回聚合清理函式或清理資源。

func provideFile(log Logger, path Path) (*os.File, func(), error) {
    f, err := os.Open(string(path))
    if err != nil {
        return nil, nil, err
    }
    cleanup := func() {
        if err := f.Close(); err != nil {
            log.Log(err)
        }
    }
    return f, cleanup, nil
}

清理函式保證在任何提供者輸入的清理函式之前被呼叫,並且必須具有簽名func()。

備用注入器語法

如果你厭倦了return foobarbaz.Foo{}, nil在注入器函式宣告的末尾寫,你可以用一個更簡潔的方式來寫 panic:

func injectFoo() Foo {
    panic(wire.Build(/* ... */))
}

注意問題

果我的依賴關係圖有兩個相同型別的依賴關係怎麼辦?

Wire 不允許在提供給 的提供者的傳遞閉包中存在一個型別的多個提供者wire.Build,因為這通常是一個錯誤。對於需要相同型別的多個依賴項的合法情況,您需要發明一種新型別來呼叫此其他依賴項。

type Foo struct { /* ... */ }
type Bar struct { /* ... */ }

func newFoo1() *Foo { /* ... */ }
func newFoo2() *Foo { /* ... */ }

解決辦法

type Foo1 Foo
type Foo2 Foo
func newFoo1() *Foo1 { /* ... */ }
func newFoo2() *Foo2 { /* ... */ }

總結

wire透過程式自動生成跟手動寫一樣程式碼,沒有使用低效的反射,效率高。
如果不小心忘記了某個provider, wire 會報出具體的錯誤, 幫忙開發者迅速定位問題。

links

https://github.com/google/wire
https://juejin.cn/post/714801...

相關文章