0%

wire使用技巧

Uploading file...ma4oe

在写go项目时,我们一般对于一些依赖的资源和类会通过依赖注入的方式来解决对象初始化流程耦合的问题。不同于diginject这两个工具通过反射来实现依赖注入,wire是通过代码生成的方式。相比之下整体流程更加易懂直观。

依赖注入

先看下以下的例子

1
2
3
4
5
6
7
8
9
10
type UserCase struct {
sqlInstance data.MySql // mysql实例
redisInstance data.Redis // redis实例
}
func NewUserCase(param1 SqlParam, param2 RedisParam) *UserCase {
return &UserCase {
sqlInstance: data.NewMysql(param1),
redisInstance: data.NewRedis(param2),
}
}

看起来是我们每个人都会写出来的业务代码,在每个业务实例中初始化依赖的数据库。但是这么做在系统变得复杂之后维护性是非常差的:

考虑一个问题,假设有个UserCase2,也依赖了一个mysql的实例,又应该怎么处理呢?很多人第一反应那还不简单,写个NewUserCase2用相同的方式初始化一次就行。但是这么做会带来两个问题:

首先,UserCaseUserCase2分开初始化,意味着每次初始化都会去实例化一个mysql的实例。此时可能会带来意想之外的bug,例如多个数据库实例对数据的操作是乱序的。虽然这也可以通过在NewMysql维持一个全局变量来解决,但是众所周知太依赖全局变量的代码一般都不是那么好维护。

其次,考虑一个更难受的情况,假设我们依次有UserCase,UserCase1, UserCase2依次包含的情况,我们就会被迫写出这样的初始化函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func NewUserCase(param1 SqlParam) *UserCase {
return &UserCase {
sqlInstance: data.NewMysql(param1),
}
}
func NewUserCase1(param1 SqlParam) *UserCase1 {
return &UserCase1 {
uc: NewUserCase(param1),
}
}
func NewUserCase2(param1 SqlParam) *UserCase2 {
return &UserCase2 {
uc: NewUserCase1(param1),
}
}

想象一下,我们此时如果给UserCase增加了另一个初始化参数,就需要把所有依赖了UserCase的结构体的初始化函数全都修改一遍,这对于一些复杂的成熟业务来说可能意味着需要修改上百个函数声明以及单元测试。

为了解决这种结构体初始化耦合的情况,有一个方法就是依赖注入,它是依赖反转的一种编码技巧。简单地说,就是把依赖的资源全都通过参数传进来,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
type UserCase struct {
}
func NewUserCase() *UserCase {
return &UserCase{}
}
type UserCase2 struct {
u *UserCase
}
func NewUserCase2(u *UserCase) *UserCase2 {
return &UserCase2{
u: u,
}
}

从代码来看就很清晰:你依赖什么东西,我就给你传进来什么东西。此时可能代码初始化会是这样的:

1
2
3
4
5
func init() {
sql := newMysqlInstance() // 初始化sql连接
uc1 := NewUserCase(sql)
uc2 := NewUserCase2(uc1)
}

使用wire简化依赖注入

很显然,从工作量上来看wire并没有简化我们的工程,init函数内该写的内容一项不少。考虑到这些代码实际上没有什么技术含量,全都是初始化对象之后传入到初始化函数中,我们可以使用wire来自动生成这个init函数。

wire是一个实现自动依赖注入的库,简而言之就是自动为我们生成上边那个init函数的。

首先安装wire:

1
2
3
4
5
# 导入到项目中
go get -u github.com/google/wire
# 安装命令
go install github.com/google/wire/cmd/wire

然后我们可以写一份这样的测试代码,它包含两个文件main.gowire.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// main.go
package main
import "fmt"
type UserCase struct {
}
func NewUserCase() *UserCase {
return &UserCase{}
}
type UserCase2 struct {
u *UserCase
}
func NewUserCase2(u *UserCase) *UserCase2 {
return &UserCase2{
u: u,
}
}
func main() {
uc2 := wireApp()
fmt.Println(uc2)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// wire.go
//go:build wireinject
// +build wireinject
// The build tag makes sure the stub is not built in the final build.
package main
import "github.com/google/wire"
// wireApp init application.
func wireApp() *UserCase2 {
// 这里因为不是真正地返回值 主要用于代码生成 所以直接用panic包一下就行
// 当然如下写法也行 只要符合golang的语法即可
// wire.Build(NewUserCase, NewUserCase2)
// return nil // 这里需要return 否则语法是错的无法生成代码
panic(wire.Build(NewUserCase, NewUserCase2))
}

为了把demo代码跑起来,执行以下步骤:

1
2
3
go mod tidy # 下载依赖
wire . # 生成依赖注入代码
go build # 编译

我们会发现多了一份新的代码wire_gen.go:

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

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

package main

// Injectors from wire.go:

// wireApp init application.
func wireApp() *UserCase2 {
userCase := NewUserCase()
userCase2 := NewUserCase2(userCase)
return userCase2
}

这里细心的读者会发现,在wire.gowire_gen.go中都生成了同样的一份函数wireApp,不难猜到其实wire_gen.go就是根据wire.go中的代码生成的。从这里不难看出wire根据我们提供的两个构造函数NewUserCaseNewUserCase2以及返回值*UserCase2推断出了合理的初始化函数并生成。

目前这里看起来挺费事的,似乎和自己手写差不多。但是再次强调一下,对于大型项目来说,许多结构的初始化前置都隐含了几十个甚至上百个的前置依赖。这么想想,自动生成就省心多了。

另外说明一下,我们在wire.gowire_gen.go开头看到的那些注释主要是为了在编译时忽略文件

1
2
3
4
// 来自wire.go 用于表示在wire注入时才会被编译
// +build wireinject
// 来自wire_gen.go 用于表示在非wire注入时才会被编译
// +build !wireinject

这一对组合保证了两个文件任意情况下只有一个生效,从而避免文件内的标识符例如wireApp出现重复定义的情况。

wire的Provider和Injector

这是wire中最重要的两个概念,在实践了之后就比较容易懂了:

Provider:生成组件的普通方法。这些方法接收所需依赖作为参数,创建组件并将其返回。简单地说就是我们定义的NewUserCaseNewUserCase2

Injector: 由wire自动生成的函数。函数内部会按根据依赖顺序调用相关privoder。也就是wire_gen.go中生成的wireApp函数。

此外,我们还可以将Provider包装起来成为一个ProviderSet来使用,这样在wire.go中我们就可以简化书写。例如修改一下main.go,增加一行:

1
var UCSet = wire.NewSet(NewUserCase, NewUserCase2)

然后将wire.gowireapp修改成这样:

1
2
3
4
func wireApp() *UserCase2 {
wire.Build(UCSet)
return nil
}

重复之前编译的步骤,会发现生成的wire_gen.go是一样的。ProviderSet的功能主要用于包装一系列的Provider集合,例如我们自己的项目在实践中一般一个package的Provider会打包成一个集合,这样可以简化wire.go的代码逻辑。(试想一下,每加一个Provider就改一次wire.go也是挺繁琐的)。

wire的高级使用技巧

返回error和清理函数

在初始化对象时,难免会出现初始化失败的情况。此外,在打开我们也会希望在实例被回收的时候调用一些清理函数,例如关闭网络连接,释放fd的情况(类似c++的析构函数)。wire中也可以支持这样的操作。稍微修改一下UserCase的样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// main.go
func NewUserCase() (*UserCase, func(), error) {
    conn, err := net.Dial("tcp", "127.0.0.1:8080")
    if err != nil {
        return nil, nil, err
    }
    return &UserCase{
            conn: conn,
        }, func() {
            conn.Close()
        }, nil
}

// ...
func main() {
uc2, cleanup, err := wireApp() // 请注意这里的wireApp返回值
if err != nil {
panic(err)
}
fmt.Println(uc2)
cleanup() // 在最后的退出环节需要显式调用cleanup
}

在上述代码中,UserCase新建了一条tcp连接,并且返回了一个清理函数,用于关闭这条连接。
之后修改一下wireApp()的实现:

1
2
3
4
// wire.go
func wireApp() (*UserCase2, func(), error) { // 请注意这里的返回值
    panic(wire.Build(UCSet))
}

重新运行一下wire命令,会发现wire_gen.go更新了:

1
2
3
4
5
6
7
8
9
10
11
12
13
// wire_gen.go
...
// wireApp init sn application.
func wireApp() (*UserCase2, func(), error) {
userCase, cleanup, err := NewUserCase()
if err != nil {
return nil, nil, err
}
userCase2 := NewUserCase2(userCase)
return userCase2, func() {
cleanup()
}, nil
}

当然,不一定全都要返回cleanup函数和error,返回UserCase和error或者UserCase和cleanup也是可以的。
可以动手实践一下,如果在NewUserCase2里也新加cleanup函数和error,最后wire_gen.go里生成的wireApp会是啥样的。

interface{}注入

一般情况下,我们在结构体中引用的不是另一个结构体,而是接口。这样对于后续的单元测试和解耦合都是很有帮助的(例如dto和do的结构)。以之前的代码做例子,我们会这么写:

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
type UserCase struct {
conn net.Conn
}

func (u *UserCase) Echo() {
}

func NewUserCase() *UserCase {
return &UserCase{}
}

type UserInterface interface {
Echo()
}

type UserCase2 struct {
u UserInterface // 请注意 这里我们引用的是UserInterface
}

// 我们希望wire能够自动将*UserCase作为u传入进来
func NewUserCase2(u UserInterface) *UserCase2 {
return &UserCase2{
u: u,
}
}

此时我们需要让wire自动用NewUserCase来注入到UserCase2中,可以使用wire.Bind修改Provider,最后main.go如下所示:

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
35
36
37
38
39
40
// main.go
package main

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

type UserCase struct {
}

func (u *UserCase) Echo() {
}

func NewUserCase() *UserCase {
return &UserCase{}
}

type UserInterface interface {
Echo()
}

type UserCase2 struct {
u UserInterface
}

func NewUserCase2(u UserInterface) *UserCase2 {
return &UserCase2{
u: u,
}
}

// 注意这里的wire.Bind 声明在这个Provider中,用*UserCase作为UserInterface
var UCSet = wire.NewSet(NewUserCase, NewUserCase2, wire.Bind(new(UserInterface), new(*UserCase)))

func main() {
uc2 := wireApp()

fmt.Println(uc2)
}

执行wire命令会发现wire_gen.go没有变化。

简化wire声明

在前例中,我们发现其实NewUserCase什么都没做,只是生成了一个空的UserCase对象,在这种情况下我们可以省略掉NewUserCase函数,写成这种形式:

1
2
var UCSet = wire.NewSet(wire.Struct(new(UserCase)), NewUserCase2, 
wire.Bind(new(UserInterface), new(*UserCase)))

同理,NewUserCase2的内容也很简单,只是多了一个成员绑定的过程,因此我们也可以不需要NewUserCase2,进一步将UCSet声明改为:

1
2
var UCSet = wire.NewSet(wire.Struct(new(UserCase)), wire.Struct(new(UserCase2), "*"),
wire.Bind(new(UserInterface), new(*UserCase)))

这里是利用了wire.Struct,其中wire.Struct(new(UserCase2),"u")表明需要新建一个UserCase2结构体并且需要注入属性u,如果全部属性都需要注入,也可以修改为wire.Struct(new(UserCase2),"*")
当然提及到全部注入,新的需求就又出现了:我想要注入全部的属性,除了特定的那一个,应该如何是好?当然也是可以的:

1
2
3
4
type UserCase2 struct {
u UserInterface
a uint32 `wire:"-"` // tag表明不需要wire注入
}

修改后的代码如下所示:

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
// main.go
package main

import (
"fmt"

"github.com/google/wire"
)

type UserCase struct {
}

func (u *UserCase) Echo() {
}

type UserInterface interface {
Echo()
}

type UserCase2 struct {
u UserInterface
a uint32 `wire:"-"`
}

var UCSet = wire.NewSet(wire.Struct(new(UserCase)), wire.Struct(new(UserCase2), "*"),
wire.Bind(new(UserInterface), new(*UserCase)))

func main() {
uc2 := wireApp()
fmt.Println(uc2)
}

照例再看一下wire_gen.go的样子:

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

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

package main

// Injectors from wire.go:

// wireApp init sn application.
func wireApp() *UserCase2 {
userCase := &UserCase{}
userCase2 := &UserCase2{
u: userCase, // 请注意这里
}
return userCase2
}

注意到wire_gen.go中操作了非公开访问的属性,这么做不会报错只是因为UserCase2main包内。正常情况下使用这种属性绑定的方式就要求属性是公开访问的。便利性有了,代价是封装性下降。应该选用什么方式就留给大家自行权衡了。

使用结构体字段作为Provider

实际开发中我们常常会有注入一个结构体的属性的需求,例如以下代码中,Config代表了一个全局的配置数据,对于不同的实例我们需要读取不同的属性:

1
2
3
4
type Config struct {
SqlParam *SqlConfig
RedisParam *RedisConfig
}

此时对于Sql的实例,只需要SqlParam即可,因此可以使用wire.FieldsOf来指定获取其中的属性:

1
2
3
4
5
6
7
8
9
10
11
type SqlInstance struct {
SqlParam *SqlConfig
...
}
func NewSqlInstance(SqlParam *SqlConfig) *SqlInstance {
return &SqlInstance{
SqlParam: SqlParam,
....
}
}
wire.Build(NewSqlInstance, wire.FieldsOf(new(Config), "SqlParam"))

参考

(google/wire: Compile-time Dependency Injection for Go (github.com))