依赖注入工具-wire
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 | package foobarbaz |
Providers 可以通过参数指定依赖项:
1 | package foobarbaz |
Providers 还可以返回 errors:
1 | package foobarbaz |
不同 Providers 可以组合成一个 ProviderSet。如果经常一起使用这些 Providers,那就很方便了。
1 | package foobarbaz |
另外,还可以将多个 ProviderSet 组合起来,形成一个新的 ProviderSet:
1 | package foobarbaz |
Injectors
Injector:能够按照依赖顺序调用 Providers 的函数。
一个应用程序就是通过 Injector 将不同的 Providers 串联起来。
使用 Wire 编写 Injector 签名,然后运行 Wire 命令,生成函数主体。
1 | // +build wireinject |
Injectors 也支持通过参数指定依赖(参数会转交给对应的 Provider),并且也可以返回 error 类型值。
wire.Build 的参数与 wire.NewSet 作用一样:它们形成 ProviderSet。这是在生成该 Injector 代码期间使用的 ProviderSet。
在 Injector 代码文件中,找到的所有 Injector 声明,都将被复制到生成的代码文件中。
在 Injector 代码文件目录下,运行 Wire 命令:
1 | wire |
Wire 将在 wire_gen.go 文件中生成 Injector 的实现,如下:
1 | // Code generated by Wire. DO NOT EDIT. |
如上所示,生成的内容跟开发人员自己所写的内容很像。该内容,在运行时,对 Wire 的依赖很小(所有生成的内容都是常规的 Go 代码,无需 Wire 即可使用)。
一旦 wire_gen.go 被创建过,后续,可以直接通过运行 go generate 来重新生成。
Wire 高级特性
以下特性都是建立在 Providers 和 Injectors 概念基础之上。
绑定接口
依赖注入常用于绑定一个接口类型的具体实现。
Wire 是通过类型标识将输入与输出进行匹配,一般,倾向于创建一个返回接口类型的 Provider。然而,这不是惯用用法,因为 Go 最佳实践是:返回具体实现。
依赖接口,返回(接口的具体)实现。
相反,我们可以在 ProviderSet 中声明接口绑定:
1 | type Fooer interface { |
wire.Bind 函数:
- 第一个参数:所需.接口类型的值.的指针。
- 第二个参数:接口类型的具体实现.的类型.的值.的指针。
结构体 Providers
可以使用 Providers 返回的不同类型,来构造不同的结构体。
使用 wire.Struct 函数构造一个结构体类型,并告诉 Injector 应该被注入到哪个字段。
Injector 将使用每个字段类型对应的 Provider 填充每个字段。
对于生成的结构体类型 S,wire.Struct 同时支持 S 和 *S。例如:
1 | type Foo int |
生成代码,如下:
1 | func injectFooBar() FooBar { |
wire.Struct 的第一个参数是指向所需的结构体类型的指针,而后续参数是要注入的字段名。
而特殊字符 用于告诉 Injector 注入到所有字段。因此,wire.Struct(new(FooBar),”“) 生成的代码与上面的示例一样。
对于上面的示例,可以通过更改 Set 来指定仅注入 MyFoo:
1 | var Set = wire.NewSet( |
生成代码如下:
1 | func injectFooBar() FooBar { |
如果 Injector 返回 *FooBar 而不是 FooBar,则生成代码如下:
1 | func injectFooBar() *FooBar { |
使用 wire:”-“ 标记字段,让 Wire 忽略此字段。这对防止 Injector 填充某些字段很有用,尤其是对 wire.Struct 使用 * 参数时。 例如:
1 | type Foo struct { |
当使用 wire.Struct(new(Foo), “*”) 构造 Foo 类型时,Wire 将忽略 mu 字段。
此外,在 wire.Struct(new(Foo), “mu”) 中显式指定一个忽略的字段,也是错误的。
绑定 Values
有时,将基本值(通常为 nil)绑定到类型很有用。
可以将值表达式添加到 ProviderSet,而不是让 Injector 依赖一次性 Provider 函数(仅被此处调用,仅调用一次)。
1 | type Foo struct { |
生成代码如下:
1 | func injectFoo() Foo { |
注意,该表达式被复制到 Injector 的函数中。对变量的引用将在 Injector 包装初始化时计算。如果表达式调用任何函数或从任何通道接收,则 Wire 将报错。
对于接口类型 values,使用 InterfaceValue:
1 | func injectReader() io.Reader { |
将结构体字段作为 Providers
有时,用户想要的 Providers 是结构体的某些字段。
如果发现自己在编写类似以下示例中的 getS 函数时(将结构体字段提升为 Provider 返回值):
1 | type Foo struct { |
可以改用 wire.FieldsOf 来直接使用这些字段,而无需编写 getS 函数:
1 | func injectedMessage() string { |
生成代码如下:
1 | func injectedMessage() string { |
按需将任意多个字段名称添加到 wire.FieldsOf 函数中。
对于给定的字段类型 T,FieldsOf 至少提供 T;如果 struct 参数是指向结构的指针,则 FieldsOf还提供 *T。
Cleanup 函数
如果 Provider 创建了一个需要清除的值(例如:关闭文件),那么,它可以返回一个闭包函数以清除资源。
如果在 Injector 的实现中稍后调用的 Provider 返回错误,则 Injector 将使用此函数将聚合的清除函数返回给调用者或清理资源。
1 | func provideFile(log Logger, path Path) (*os.File, func(), error) { |
确保在 Provider 的任何输入的清除功能之前调用清除功能,并且该清除功能必须具有签名 func()。
另一种 Provider 语法
可以使用 panic 来编写更简洁的 Provider:
1 | func injectFoo() Foo { |