Wire
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 install github.com/google/wire/cmd/wire@latest安装源代码依赖
go get github.com/google/wire入门
wire 中依赖注入基于两个元素,provier和injector。
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
}下面的MysqlClient和System的 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{}) Bindingvar 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 stringwire 将无法区分这些参数该如何注入,为了避免冲突,可以使用类型别名。
