依赖注入wire

2k 词

wire 简介

Wire 是一种代码生成工具,其使用依赖注入自动连接组件。
在 Wire 中,组件之间的依赖关系是通过函数参数来表示的。
组件 B 依赖组件 A,那么构造组件 B 的函数大致需要定义为:NewB(a A) B。
这里,鼓励显式初始化组件 A,而不是,定义一个组件 A 全局变量,然后不通过传参而直接在 NewB 函数中使用全局变量。

wire 安装

1
go get github.com/google/wire/cmd/wire

确保 $GOPATH/bin 已添加到 $PATH 环境变量。

wire 基础概念

在 wire 中,有两个核心概念:Providers、Injectors。

Providers

wire 的主要机制是 Provider:具有返回值的函数。
这些函数都是常规的 go 代码。

1
2
3
4
5
6
7
8
9
10
package foobarbaz

type Foo struct {
X int
}

// ProvideFoo 函数返回一个 Foo 结构体值。
func ProvideFoo() Foo {
return Foo{X: 42}
}

Providers 可以通过参数指定依赖项:

1
2
3
4
5
6
7
8
9
10
package foobarbaz

type Bar struct {
X int
}

// ProvideBar 函数通过参数指定依赖 Foo 结构体。
func ProvideBar(foo Foo) Bar {
return Bar{X: -foo.X}
}

Providers 还可以返回 errors:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package foobarbaz

import (
"context"
"errors"
)

type Baz struct {
X int
}

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
}

不同 Providers 可以组合成一个 ProviderSet。如果经常一起使用这些 Providers,那就很方便了。

1
2
3
4
5
6
7
package foobarbaz

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

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

另外,还可以将多个 ProviderSet 组合起来,形成一个新的 ProviderSet:

1
2
3
4
5
6
7
package foobarbaz

import (
"example.com/some/other/pkg"
)

var MegaSet = wire.NewSet(SuperSet, pkg.OtherSet)

Injectors

Injector:能够按照依赖顺序调用 Providers 的函数。
一个应用程序就是通过 Injector 将不同的 Providers 串联起来。
使用 Wire 编写 Injector 签名,然后运行 Wire 命令,生成函数主体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// +build wireinject
// 此 build tag 用于确保此代码文件在最终构建阶段不会被纳入构建。

package main

import (
"context"

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

func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
wire.Build(foobarbaz.SuperSet)
return foobarbaz.Baz{}, nil
}

Injectors 也支持通过参数指定依赖(参数会转交给对应的 Provider),并且也可以返回 error 类型值。
wire.Build 的参数与 wire.NewSet 作用一样:它们形成 ProviderSet。这是在生成该 Injector 代码期间使用的 ProviderSet。
在 Injector 代码文件中,找到的所有 Injector 声明,都将被复制到生成的代码文件中。
在 Injector 代码文件目录下,运行 Wire 命令:

1
wire

Wire 将在 wire_gen.go 文件中生成 Injector 的实现,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Code generated by Wire. DO NOT EDIT.

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

package main

import (
"example.com/foobarbaz"
)

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 的依赖很小(所有生成的内容都是常规的 Go 代码,无需 Wire 即可使用)。
一旦 wire_gen.go 被创建过,后续,可以直接通过运行 go generate 来重新生成。
image.png

Wire 高级特性

以下特性都是建立在 Providers 和 Injectors 概念基础之上。

绑定接口

依赖注入常用于绑定一个接口类型的具体实现。
Wire 是通过类型标识将输入与输出进行匹配,一般,倾向于创建一个返回接口类型的 Provider。然而,这不是惯用用法,因为 Go 最佳实践是:返回具体实现
依赖接口,返回(接口的具体)实现。
相反,我们可以在 ProviderSet 中声明接口绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
type Fooer interface {
Foo() string
}

type MyFooer string

// Fooer 接口的具体实现
func (b *MyFooer) Foo() string {
return string(*b)
}

// 返回的是 Fooer 接口的具体实现
func provideMyFooer() *MyFooer {
b := new(MyFooer)
*b = "Hello, World!"
return b
}

type Bar string

// 依赖 Fooer 接口
func provideBar(f Fooer) string {
return f.Foo()
}

var Set = wire.NewSet(
provideMyFooer,

// 接口绑定,其实就是声明 *MyFooer 是 Fooer 接口类型。
wire.Bind(new(Fooer), new(*MyFooer)),

// 这样,provideMyFooer 返回的 *MyFooer 就可以被当成 Fooer 接口类型,注入到 provideBar 中。
provideBar,
)

wire.Bind 函数:

  • 第一个参数:所需.接口类型的值.的指针。
  • 第二个参数:接口类型的具体实现.的类型.的值.的指针。

结构体 Providers

可以使用 Providers 返回的不同类型,来构造不同的结构体。
使用 wire.Struct 函数构造一个结构体类型,并告诉 Injector 应该被注入到哪个字段。
Injector 将使用每个字段类型对应的 Provider 填充每个字段。
对于生成的结构体类型 S,wire.Struct 同时支持 S 和 *S。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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"))

生成代码,如下:

1
2
3
4
5
6
7
8
9
func injectFooBar() FooBar {
foo := ProvideFoo()
bar := ProvideBar()
fooBar := FooBar{
MyFoo: foo,
MyBar: bar,
}
return fooBar
}

wire.Struct 的第一个参数是指向所需的结构体类型的指针,而后续参数是要注入的字段名。
而特殊字符 * 用于告诉 Injector 注入到所有字段。因此,wire.Struct(new(FooBar),”*”) 生成的代码与上面的示例一样。
对于上面的示例,可以通过更改 Set 来指定仅注入 MyFoo:

1
2
3
4
var Set = wire.NewSet(
ProvideFoo,
wire.Struct(new(FooBar), "MyFoo"))

生成代码如下:

1
2
3
4
5
6
7
8
func injectFooBar() FooBar {
foo := ProvideFoo()
fooBar := FooBar{
MyFoo: foo,
}
return fooBar
}

如果 Injector 返回 *FooBar 而不是 FooBar,则生成代码如下:

1
2
3
4
5
6
7
8
func injectFooBar() *FooBar {
foo := ProvideFoo()
fooBar := &FooBar{
MyFoo: foo,
}
return fooBar
}

使用 wire:”-“ 标记字段,让 Wire 忽略此字段。这对防止 Injector 填充某些字段很有用,尤其是对 wire.Struct 使用 * 参数时。 例如:

1
2
3
4
5
type Foo struct {
mu sync.Mutex `wire:"-"`
Bar Bar
}

当使用 wire.Struct(new(Foo), “*”) 构造 Foo 类型时,Wire 将忽略 mu 字段。
此外,在 wire.Struct(new(Foo), “mu”) 中显式指定一个忽略的字段,也是错误的。

绑定 Values

有时,将基本值(通常为 nil)绑定到类型很有用。
可以将值表达式添加到 ProviderSet,而不是让 Injector 依赖一次性 Provider 函数(仅被此处调用,仅调用一次)。

1
2
3
4
5
6
7
8
9
type Foo struct {
X int
}

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

生成代码如下:

1
2
3
4
5
6
7
8
9
func injectFoo() Foo {
foo := _wireFooValue
return foo
}

var (
_wireFooValue = Foo{X: 42}
)

注意,该表达式被复制到 Injector 的函数中。对变量的引用将在 Injector 包装初始化时计算。如果表达式调用任何函数或从任何通道接收,则 Wire 将报错。
对于接口类型 values,使用 InterfaceValue:

1
2
3
4
5
func injectReader() io.Reader {
wire.Build(wire.InterfaceValue(new(io.Reader), os.Stdin))
return nil
}

将结构体字段作为 Providers

有时,用户想要的 Providers 是结构体的某些字段。
如果发现自己在编写类似以下示例中的 getS 函数时(将结构体字段提升为 Provider 返回值):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type Foo struct {
S string
N int
F float64
}

func getS(foo Foo) string {
// 不好的方式!使用 wire.FieldsOf 代替。
return foo.S
}

func provideFoo() Foo {
return Foo{ S: "Hello, World!", N: 1, F: 3.14 }
}

func injectedMessage() string {
wire.Build(
provideFoo,
getS)
return ""
}

可以改用 wire.FieldsOf 来直接使用这些字段,而无需编写 getS 函数:

1
2
3
4
5
6
7
func injectedMessage() string {
wire.Build(
provideFoo,
wire.FieldsOf(new(Foo), "S"))
return ""
}

生成代码如下:

1
2
3
4
5
6
func injectedMessage() string {
foo := provideFoo()
string2 := foo.S
return string2
}

按需将任意多个字段名称添加到 wire.FieldsOf 函数中。
对于给定的字段类型 T,FieldsOf 至少提供 T;如果 struct 参数是指向结构的指针,则 FieldsOf还提供 *T。

Cleanup 函数

如果 Provider 创建了一个需要清除的值(例如:关闭文件),那么,它可以返回一个闭包函数以清除资源。
如果在 Injector 的实现中稍后调用的 Provider 返回错误,则 Injector 将使用此函数将聚合的清除函数返回给调用者或清理资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
func provideFile(log Logger, path Path) (*os.File, func(), error) {
f, err := os.Open(string(path))
if err != nil {
return nil, nil, err
}
cleanup := func() {
if err := f.Close(); err != nil {
log.Log(err)
}
}
return f, cleanup, nil
}

确保在 Provider 的任何输入的清除功能之前调用清除功能,并且该清除功能必须具有签名 func()。

另一种 Provider 语法

可以使用 panic 来编写更简洁的 Provider:

1
2
3
4
func injectFoo() Foo {
panic(wire.Build(/* ... */))
}

留言