Wire

寒江蓑笠翁大约 13 分钟

Wire

wire是谷歌开源的一个依赖注入工具,依赖注入这个概念在Java的Spring框架中相当盛行,go中也有一些依赖注入库,例如Uber开源的dig。不过wire的依赖注入理念并不是基于语言的反射机制,严格来说,wire其实是一个代码生成器,依赖注入的理念只体现在使用上,如果有问题的话,在代码生成期间就能找出来。

仓库地址:google/wire: Compile-time Dependency Injection for Go (github.com)open in new window

文档地址:wire/docs/guide.md at main · google/wire (github.com)open in new window

安装

安装代码生成工具

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

安装源代码依赖

go get github.com/google/wire

入门

wire中依赖注入基于两个元素,provierinjector

provier可以是开发者提供一个构造器,如下,Provider必须是对外暴露的。

package foobarbaz

type Foo struct {
    X int
}

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

带参数

package foobarbaz

// ...

type Bar struct {
    X int
}

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

也可以带有参数和返回值

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进行组合

package foobarbaz

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

// ...

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

提示

wire对provider的返回值有如下规定

  • 第一个返回值是provider提供的值
  • 第二个返回值必须是func() | error
  • 第三个返回值,如果第二个返回值是func,那么第三个返回值必须是error

injector是由wire生成的一个函数,它负责按照指定的顺序去调用provider,injector的签名由开发者来定义,wire生成具体的函数体,通过调用wire.Build来声明,这个声明不应该被调用,更不应该被编译。

func Build(...interface{}) string {
	return "implementation not generated, run wire"
}
// +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,内容如下

// 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

var ServerProviderSet = wire.NewSet(NewHttpserver)

type HttpServer struct {
	net.Addr
}

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

下面的MysqlClientSystem的provider同理

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: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。然后执行如下命令,有如下输出即生成成功。

$ wire
$ wire: golearn: wrote /golearn/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 (
	"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。

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在依赖注入时,是根据类型匹配来实现的。

ype 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能够正确匹配,我们可以将两种类型绑定,如下

第一个参数是具体的接口指针类型,第二个是具体实现的指针类型。

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

值绑定

在使用wire.Build时,可以不用provider提供值,也可以使用wire.Value来提供一个具体的值。wire.Value支持表达式来构造值,这个表达式在生成代码时会被复制到injector中,如下。

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

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

结构体构造

在providerset中,可以使用wire.Struct来利用其他provider的返回值构建一个指定类型的结构体。

第一个参数应该传入结构体指针类型,后续是字段名称。

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

示例如下

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可能如下所示

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

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

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

默认是构造结构体类型,如果想要构造指针类型,可以修改injector签名的返回值

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

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

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

Cleanup

如果provider构造的一个值在使用后需要进行收尾工作(比如关闭一个文件),provider可以返回一个闭包来进行这样的操作,injector并不会调用这个cleanup函数,具体何时调用交给injector的调用者,如下。

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
}

实际生成的代码可能如下

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的入参最好不要类型重复,尤其是对于一些基础类型

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将无法区分这些参数该如何注入,为了避免冲突,可以使用类型别名。