首页 > 数据库 > 一文读懂 Go官方的 Wire
2020
03-13

一文读懂 Go官方的 Wire

Wire 是啥

Wire[1] 是一个轻巧的 Golang 依赖注入工具。它由 Go Cloud 团队开发,通过自动生成代码的方式在编译期完成依赖注入。

依赖注入[2]是保持软件 “低耦合、易维护” 的重要设计准则之一。

此准则被广泛应用在各种开发平台之中,有很多与之相关的优秀工具。

其中最著名的当属 Spring[3],Spring IOC 作为框架的核心功能对 Spring 的发展到今天统治地位起了决定性作用。

事实上, 软件开发 S.O.L.I.D 原则[4] 中的“D”, 就专门指代这个话题。


Wire 的特点

依赖注入很重要,所以 Golang 社区中早已有人开发了相关工具, 比如来自 Uber 的 dig[5] 、来自 Facebook 的 inject[6] 。他们都通过反射机制实现了运行时依赖注入。

为什么 Go Cloud 团队还要重造一遍轮子呢?因为在他们看来上述类库都不符合 Go 的哲学:

Clear is better than clever[7] Reflection is never clear.[8]

— Rob Pike

作为一个代码生成工具, Wire 可以生成 Go 源码并在编译期完成依赖注入。它不需要反射机制或 Service Locators[9] 。后面会看到, Wire 生成的代码与手写无异。这种方式带来一系列好处:

  1. 方便 debug,若有依赖缺失编译时会报错
  2. 因为不需要 Service Locators, 所以对命名没有特殊要求
  3. 避免依赖膨胀。生成的代码只包含被依赖的代码,而运行时依赖注入则无法作到这一点
  4. 依赖关系静态存于源码之中, 便于工具分析与可视化

团队对 Wire 设计的仔细权衡可以参看 Go Blog[10] 

虽然目前 Wire 只发布了 v0.4 .0,但已经比较完备地达成了团队设定的目标。预计后面不会有什么大变化了。从团队傲娇的声明中可以看出这一点:

It works well for the tasks it was designed to perform, and we prefer to keep it as simple as possible.

We’ll not be accepting new features at this time, but will gladly accept bug reports and fixes.

 Wire team[11]


上手使用

安装很简单,运行go get github.com/google/wire/cmd/wire 之后, wire 命令行工具 将被安装到 $GOPATH/bin 。只要确保 $GOPATH/bin  $PATH中, wire命令就可以在任何目录调用了。

在进一步介绍之前, 需要先解释 wire 中的两个核心概念:Provider 和 Injector:

Provider: 生成组件的普通方法。这些方法接收所需依赖作为参数,创建组件并将其返回。

组件可以是对象或函数 —— 事实上它可以是任何类型,但单一类型在整个依赖图中只能有单一 provider。因此返回 int 类型的 provider 不是个好主意。对于这种情况, 可以通过定义类型别名来解决。例如先定义type Category int ,然后让 provider 返回 Category 类型

典型 provider 示例如下:

// DefaultConnectionOpt provide default connection option
func DefaultConnectionOpt()*ConnectionOpt{...}// NewDb provide an Db object
func NewDb(opt *ConnectionOpt)(*Db, error){...}// NewUserLoadFunc provide a function which can load user
func NewUserLoadFunc(db *Db)(func(int) *User, error){...}

实践中, 一组业务相关的 provider 时常被放在一起组织成 ProviderSet,以方便维护与切换。

var DbSet = wire.NewSet(DefaultConnectionOpt, NewDb)

Injector: 由wire自动生成的函数。函数内部会按根据依赖顺序调用相关 privoder 。

为了生成此函数, 我们在 wire.go (文件名非强制,但一般约定如此)文件中定义 injector 函数签名。然后在函数体中调用wire.Build ,并以所需 provider 作为参数(无须考虑顺序)。

由于wire.go中的函数并没有真正返回值,为避免编译器报错, 简单地用panic函数包装起来即可。不用担心执行时报错, 因为它不会实际运行,只是用来生成真正的代码的依据。一个简单的 wire.go 示例

// +build wireinject

package main

import "github.com/google/wire"

func UserLoader()(func(int)*User, error){
   panic(wire.Build(NewUserLoadFunc, DbSet))
}

var DbSet = wire.NewSet(DefaultConnectionOpt, NewDb)

有了这些代码以后,运行 wire 命令将生成 wire_gen.go 文件,其中保存了 injector 函数的真正实现。wire.go 中若有非 injector 的代码将被原样复制到 wire_gen.go 中(虽然技术上允许,但不推荐这样作)。生成代码如下:

// Code generated by Wire. DO NOT EDIT.

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

package main

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

// Injectors from wire.go:

func UserLoader() (func(int) *User, error) {
   connectionOpt := DefaultConnectionOpt()
   db, err := NewDb(connectionOpt)
   if err != nil {
      return nil, err
   }
   v, err := NewUserLoadFunc(db)
   if err != nil {
      return nil, err
   }
   return v, nil
}

// wire.go:

var DbSet = wire.NewSet(DefaultConnectionOpt, NewDb)

上述代码有两点值得关注:

  1. wire.go 第一行 // ***+build\*** wireinject ,这个 build tag[12] 确保在常规编译时忽略 wire.go 文件(因为常规编译时不会指定 wireinject 标签)。与之相对的是 wire_gen.go 中的 //***+build\*** !wireinject 。两组对立的 build tag 保证在任意情况下, wire.go 与 wire_gen.go 只有一个文件生效, 避免了“UserLoader 方法被重复定义”的编译错误
  2. 自动生成的 UserLoader 代码包含了 error 处理。与我们手写代码几乎相同。对于这样一个简单的初始化过程, 手写也不算麻烦。但当组件数达到几十、上百甚至更多时, 自动生成的优势就体现出来了。

要触发“生成”动作有两种方式:go generate  wire 。前者仅在 wire_gen.go 已存在的情况下有效(因为 wire_gen.go 的第三行 //***go:generate\*** wire),而后者在任何时候都有可以调用。并且后者有更多参数可以对生成动作进行微调, 所以建议始终使用 wire 命令。

然后我们就可以使用真正的 injector 了, 例如:

package main

import "log"

func main() {
   fn, err := UserLoader()
   if err != nil {
      log.Fatal(err)
   }
   user := fn(123)
   ...
}

如果不小心忘记了某个 provider, wire 会报出具体的错误, 帮忙开发者迅速定位问题。例如我们修改 wire.go ,去掉其中的NewDb

// +build wireinject

package main

import "github.com/google/wire"

func UserLoader()(func(int)*User, error){
   panic(wire.Build(NewUserLoadFunc, DbSet))
}

var DbSet = wire.NewSet(DefaultConnectionOpt) //forgot add Db provider

将会报出明确的错误:“no provider found for *example.Db”

wire: /usr/example/wire.go:7:1: inject UserLoader: no provider found for *example.Db
      needed by func(int) *example.User in provider "NewUserLoadFunc" (/usr/example/provider.go:24:6)
wire: example: generate failed
wire: at least one generate failure

同样道理, 如果在 wire.go 中写入了未使用的 provider , 也会有明确的错误提示。


高级功能

谈过基本用法以后, 我们再看看高级功能

*接口注入*

有时需要自动注入一个接口, 这时有两个选择:

  1. 较直接的作法是在 provider 中生成具体类, 然后返回接口类型。但这不符合Golang 代码规范[13]。一般不采用
  2. 让 provider 返回具体类,但在 injector 声明环节作文章,将类绑定成接口,例如:
// FooInf, an interface
// FooClass, an class which implements FooInf
// fooClassProvider, a provider function that provider *FooClassvar set = wire.NewSet(
    fooClassProvider,
    wire.Bind(new(FooInf), new(*FooClass) // bind class to interface
)

*属性自动注入*

有时我们不需什么特定的初始化工作, 只是简单地创建一个对象实例, 为其指定属性赋值,然后返回。当属性多的时候,这种工作会很无聊。

// provider.gotype App struct {
    Foo *Foo
    Bar *Bar
}func DefaultApp(foo *Foo, bar *Bar)*App{
    return &App{Foo: foo, Bar: bar}
}
// wire.go
...
wire.Build(provideFoo, provideBar, DefaultApp)
...

wire.Struct 可以简化此类工作, 指定属性名来注入特定属性:

wire.Build(provideFoo, provideBar, wire.Struct(new(App),"Foo","Bar")

如果要注入全部属性,则有更简化的写法:

wire.Build(provideFoo, provideBar, wire.Struct(new(App), "*")

如果 struct 中有个别属性不想被注入,那么可以修改 struct 定义:

type App struct {
    Foo *Foo
    Bar *Bar
    NoInject int `wire:"-"`
}

这时 NoInject 属性会被忽略。与常规 provider 相比, wire.Struct 提供一项额外的灵活性:它能适应指针与非指针类型,根据需要自动调整生成的代码。

大家可以看到wire.Struct的确提供了一些便利。但它要求注入属性可公开访问, 这导致对象暴露本可隐藏的细节。

好在这个问题可以通过上面提到的“接口注入”来解决。用 wire.Struct 创建对象,然后将其类绑定到接口上。至于在实践中如何权衡便利性和封装程度,则要具体情况具体分析了。

*值绑定*

虽不常见,但有时需要为基本类型的属性绑定具体值, 这时可以使用wire.Value :

// provider.go
type Foo struct {
    X int
}// wire.go
...
wire.Build(wire.Value(Foo{X: 42}))
...

为接口类型绑定具体值,可以使用 wire.InterfaceValue :

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

*把对象属性用作 Provider*

有时我们只是需要用某个对象的属性作为 Provider,例如

// provider
func provideBar(foo Foo)*Bar{
    return foo.Bar
}
// injector
...
wire.Build(provideFoo, provideBar)
...

这时可以用 wire.FieldsOf 加以简化,省掉啰嗦的 provider:

wire.Build(provideFoo, wire.FieldsOf(new(Foo), "Bar"))

 wire.Struct 类似, wire.FieldsOf 也会自动适应指针/非指针的注入请求

*清理函数*

前面提到若 provider 和 injector 函数有返回错误, 那么 wire 会自动处理。除此以外,wire 还有另一项自动处理能力:清理函数。

所谓清理函数是指型如 func() 的闭包, 它随 provider 生成的组件一起返回, 确保组件所需资源可以得到清理。

清理函数典型的应用场景是文件资源和网络连接资源,例如:

type App struct {
   File *os.File
   Conn net.Conn
}

func provideFile() (*os.File, func(), error) {
   f, err := os.Open("foo.txt")
   if err != nil {
      return nil, nil, err
   }
   cleanup := func() {
      if err := f.Close(); err != nil {
         log.Println(err)
      }
   }
   return f, cleanup, nil
}

func provideNetConn() (net.Conn, func(), error) {
   conn, err := net.Dial("tcp", "foo.com:80")
   if err != nil {
      return nil, nil, err
   }
   cleanup := func() {
      if err := conn.Close(); err != nil {
         log.Println(err)
      }
   }
   return conn, cleanup, nil
}

上述代码定义了两个 provider 分别提供了文件资源和网络连接资源

wire.go

// +build wireinject

package main

import "github.com/google/wire"

func NewApp() (*App, func(), error) {
   panic(wire.Build(
      provideFile,
      provideNetConn,
      wire.Struct(new(App), "*"),
   ))
}

注意由于 provider 返回了清理函数, 因此 injector 函数签名也必须返回,否则将会报错

wire_gen.go

// Code generated by Wire. DO NOT EDIT.

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

package main

// Injectors from wire.go:

func NewApp() (*App, func(), error) {
   file, cleanup, err := provideFile()
   if err != nil {
      return nil, nil, err
   }
   conn, cleanup2, err := provideNetConn()
   if err != nil {
      cleanup()
      return nil, nil, err
   }
   app := &App{
      File: file,
      Conn: conn,
   }
   return app, func() {
      cleanup2()
      cleanup()
   }, nil
}

生成代码中有两点值得注意:

  1.  provideNetConn 出错时会调用 cleanup() , 这确保了即使后续处理出错也不会影响前面已分配资源的清理。
  2. 最后返回的闭包自动组合了 cleanup2()  cleanup() 。意味着无论分配了多少资源, 只要调用过程不出错,他们的清理工作就会被集中到统一的清理函数中。最终的清理工作由 injector 的调用者负责

可以想像当几十个清理函数的组合在一起时, 手工处理上述两个场景是非常繁琐且容易出错的。wire 的优势再次得以体现。

然后就可以使用了:

func main() {
   app, cleanup, err := NewApp()
   if err != nil {
      log.Fatal(err)
   }
   defer cleanup()
   ...
}

注意 main 函数中的 defer cleanup() ,它确保了所有资源最终得到回收


总结

以上详细介绍了 wire 的概念、特点、使用方法及各种高级特性。

希望能帮你掌握这个小巧却强大的工具。

扫码芷若 获取免费视频学习资料

编程学习

查 看2019高级编程视频教程免费获取