Wire 是一个轻巧的 Golang 依赖注入工具。它由 Go Cloud 团队开发,通过自动生成代码的方式在编译期完成依赖注入Uber 的 dig 、来自 Facebook 的 inject 。他们都通过反射机制实现了运行时依赖注入。


 Wire 生成的代码与手写无异。这种方式带来一系列好处:

  1. 方便 debug,若有依赖缺失编译时会报错

  2. 因为不需要 Service Locators, 所以对命名没有特殊要求

  3. 避免依赖膨胀。生成的代码只包含被依赖的代码,而运行时依赖注入则无法作到这一点

  4. 依赖关系静态存于源码之中, 便于工具分析与可视化


运行go get github.com/google/wire/cmd/wire 之后, wire 命令行工具 将被安装到 $GOPATH/bin 。


wire 中的两个核心概念:Provider 和 Injector:

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

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



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

var DbSet = wire.NewSet(DefaultConnectionOpt, NewDb)


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

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

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


wire.go

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




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

  1. wire.go 第一行 // ***+build\*** wireinject ,这个 build tag 确保在常规编译时忽略 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 代码规范。一般不采用

  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 源码分析-LMLPHP

wire 源码分析-LMLPHP


本文分享自微信公众号 - golang算法架构leetcode技术php(golangLeetcode)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

07-02 20:04