Skip to content

Wire

wire 是谷歌開源的一個依賴注入工具,依賴注入這個概念在 Java 的 Spring 框架中相當盛行,go 中也有一些依賴注入庫,例如 Uber 開源的 dig。不過 wire 的依賴注入理念並不是基於語言的反射機制,嚴格來說,wire 其實是一個代碼生成器,依賴注入的理念只體現在使用上,如果有問題的話,在代碼生成期間就能找出來。

倉庫地址:google/wire: Compile-time Dependency Injection for Go (github.com)

文檔地址:wire/docs/guide.md at main · google/wire (github.com)

安裝

安裝代碼生成工具

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

安裝源代碼依賴

go get github.com/google/wire

入門

wire 中依賴注入基於兩個元素,provierinjector

provier可以是開發者提供一個構造器,如下,Provider 必須是對外暴露的。

go
package foobarbaz

type Foo struct {
    X int
}

// 構造Foo
func ProvideFoo() Foo {
    return Foo{X: 42}
}

帶參數

go
package foobarbaz

// ...

type Bar struct {
    X int
}

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

也可以帶有參數和返回值

go
package foobarbaz

import (
    "context"
    "errors"
)

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
}

也可以對 proiver 進行組合

go
package foobarbaz

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

// ...

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

TIP

wire 對 provider 的返回值有如下規定

  • 第一個返回值是 provider 提供的值
  • 第二個返回值必須是func() | error
  • 第三個返回值,如果第二個返回值是func,那麼第三個返回值必須是error

injector是由 wire 生成的一個函數,它負責按照指定的順序去調用 provider,injector 的簽名由開發者來定義,wire 生成具體的函數體,通過調用wire.Build來聲明,這個聲明不應該被調用,更不應該被編譯。

go
func Build(...interface{}) string {
  return "implementation not generated, run wire"
}
go
// +build wireinject
// The build tag makes sure the stub is not built in the final build.

package main

import (
    "context"

    "github.com/google/wire"
    "example.com/foobarbaz"
)

// 定義的injector
func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
    wire.Build(foobarbaz.MegaSet)
    return foobarbaz.Baz{}, nil
}

然後執行

wire

就會生成wire_gen.go,內容如下

go
// Code generated by Wire. DO NOT EDIT.

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

package main

import (
    "example.com/foobarbaz"
)

// 實際生成的injector
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
}

生成的代碼對於 wire 幾乎沒有任何依賴,不需要 wire 也可以正常工作,並且在後續執行go generate就可以再次生成,之後,開發者通過調用實際生成的 injector 傳入對應的參數完成依賴注入。是不是整個過程的代碼相當簡單,感覺好像就是提供幾個構造器,然後生成一個調用構造器的函數,最後再調用這個函數傳入參數,好像也沒做什麼特別復雜的事情,手寫一樣可以,沒錯就是這樣,wire 就是做的這樣一件簡單的事情,只是由手寫變成了自動生成。按照 wire 的理念,依賴注入本就是應該如此簡單的一個事情,不應復雜化。

示例

下面來通過一個案例加深一下理解,這是一個初始化 app 的例子。

HttpServer的 provider 接收一個net.Addr參數,返回指針和error

go
var ServerProviderSet = wire.NewSet(NewHttpserver)

type HttpServer struct {
  net.Addr
}

func NewHttpserver(addr net.Addr) (*HttpServer, error) {
  return &HttpServer{addr}, nil
}

下面的MysqlClientSystem的 provider 同理

go
var DataBaseProviderSet = wire.NewSet(NewMysqlClient)

type MysqlClient struct {
}

var SystemSet = wire.NewSet(NewApp)

type System struct {
  server *HttpServer
  data   *MysqlClient
}

func (s *System) Run() {
  log.Printf("app run on %s", s.server.String())
}

func NewApp(server *HttpServer, data *MysqlClient) (System, error) {
  return System{server: server, data: data}, nil
}

provider 定義完畢後,需要定義 injector,最好新建一個wire.go文件來定義

go
//go:build wireinject
// +build wireinject

package main

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

// 定義injector
func initSystemServer(serverAddr net.Addr, dataAddr string) (System, error) {
  // 按照順序調用provider
  panic(wire.Build(DataBaseProviderSet, ServerProviderSet, SystemSet))
}

+build wireinject是為了在編譯時忽略掉此 injector。然後執行如下命令,有如下輸出即生成成功。

sh
$ wire
$ wire: golearn: wrote /golearn/wire_gen.go

生成後的代碼如下

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 (
  "net"
)

// Injectors from wire.go:

// 定義injector
func initSystemServer(serverAddr net.Addr, dataAddr string) (System, error) {
  httpServer, err := NewHttpserver(serverAddr)
  if err != nil {
    return System{}, err
  }
  mysqlClient, err := NewMysqlClient(dataAddr)
  if err != nil {
    return System{}, err
  }
  system, err := NewApp(httpServer, mysqlClient)
  if err != nil {
    return System{}, err
  }
  return system, nil
}

可以看到邏輯很清晰,調用順序也是正確的,最後通過生成的 injector 來啟動 app。

go
package main

import (
  "github.com/google/wire"
  "log"
  "net"
  "net/netip"
)

func main() {
  server, err := initSystemServer(
    net.TCPAddrFromAddrPort(netip.MustParseAddrPort("127.0.0.1:8080")),
    "mysql:localhost:3306/test")
  if err != nil {
    panic(err)
  }
  server.Run()
}

最後輸出如下

2023/08/01 19:20:48 app run on 127.0.0.1:8080

這就是一個非常簡單的使用案例。

高級用法

接口綁定

有時候,依賴注入時會將一個具體的實現注入到接口上。wire 在依賴注入時,是根據類型匹配來實現的。

go
type Fooer interface {
    Foo() string
}

type MyFooer string

func (b *MyFooer) Foo() string {
    return string(*b)
}

func provideMyFooer() *MyFooer {
    b := new(MyFooer)
    *b = "Hello, World!"
    return b
}

type Bar string

func provideBar(f Fooer) string {
    // f will be a *MyFooer.
    return f.Foo()
}

providerprovideBar的參數是一個接口類型,它的實際上是*MyFooer,為了讓代碼生成時 provider 能夠正確匹配,我們可以將兩種類型綁定,如下

第一個參數是具體的接口指針類型,第二個是具體實現的指針類型。

go
func Bind(iface, to interface{}) Binding
go
var Set = wire.NewSet(
    provideMyFooer,
    wire.Bind(new(Fooer), new(*MyFooer)),
    provideBar)

值綁定

在使用wire.Build時,可以不用 provider 提供值,也可以使用wire.Value來提供一個具體的值。wire.Value支持表達式來構造值,這個表達式在生成代碼時會被復制到 injector 中,如下。

go
type Foo struct {
    X int
}

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

生成的 injector

func injectFoo() Foo {
    foo := _wireFooValue
    return foo
}

var (
    _wireFooValue = Foo{X: 42}
)

如果想要綁定一個接口類型的值,可以使用wire.InterfaceValue

go
func injectReader() io.Reader {
    wire.Build(wire.InterfaceValue(new(io.Reader), os.Stdin))
    return nil
}

結構體構造

在 providerset 中,可以使用wire.Struct來利用其他 provider 的返回值構建一個指定類型的結構體。

第一個參數應該傳入結構體指針類型,後續是字段名稱。

go
func Struct(structType interface{}, fieldNames ...string) StructProvider

示例如下

go
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"))

func injectFooBar() FoodBar {
    wire.Build(Set)
}

生成的 injector 可能如下所示

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

如果想要填充所有字段,可以使用*,例如

go
wire.Struct(new(FooBar), "*")

默認是構造結構體類型,如果想要構造指針類型,可以修改 injector 簽名的返回值

func injectFooBar() *FoodBar {
    wire.Build(Set)
}

如果想要忽略掉字段,可以加 tag,如下所示

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

Cleanup

如果 provider 構造的一個值在使用後需要進行收尾工作(比如關閉一個文件),provider 可以返回一個閉包來進行這樣的操作,injector 並不會調用這個 cleanup 函數,具體何時調用交給 injector 的調用者,如下。

go
type Data struct {
  // TODO wrapped database client
}

// NewData .
func NewData(c *conf.Data, logger log.Logger) (*Data, func(), error) {
  cleanup := func() {
    log.NewHelper(logger).Info("closing the data resources")
  }
  return &Data{}, cleanup, nil
}

實際生成的代碼可能如下

go
func wireApp(confData *conf.Data, logger log.Logger) (func(), error) {
    dataData, cleanup, err := data.NewData(confData, logger)
    if err != nil {
       return nil, nil, err
    }
    // inject data
    // ...
    return app, func() {
       cleanup()
    }, nil
}

類型重復

provider 的入參最好不要類型重復,尤其是對於一些基礎類型

go
type FooBar struct {
  foo string
  bar string
}

func NewFooBar(foo string, bar string) FooBar {
  return FooBar{
      foo: foo,
      bar: bar,
  }
}

func InitializeFooBar(a string, b string) FooBar {
    panic(wire.Build(NewFooBar))
}

這種情況下生成代碼會報錯

provider has multiple parameters of type string

wire 將無法區分這些參數該如何注入,為了避免沖突,可以使用類型別名。

Golang學習網由www.golangdev.cn整理維護