在写go
项目时,我们一般对于一些依赖的资源和类会通过依赖注入的方式来解决对象初始化流程耦合的问题。不同于dig
和inject
这两个工具通过反射来实现依赖注入,wire
是通过代码生成的方式。相比之下整体流程更加易懂直观。
依赖注入 先看下以下的例子
1 2 3 4 5 6 7 8 9 10 type UserCase struct { sqlInstance data.MySql redisInstance data.Redis }func NewUserCase (param1 SqlParam, param2 RedisParam) *UserCase { return &UserCase { sqlInstance: data.NewMysql(param1), redisInstance: data.NewRedis(param2), } }
看起来是我们每个人都会写出来的业务代码,在每个业务实例中初始化依赖的数据库。但是这么做在系统变得复杂之后维护性是非常差的:
考虑一个问题,假设有个UserCase2
,也依赖了一个mysql
的实例,又应该怎么处理呢?很多人第一反应那还不简单,写个NewUserCase2
用相同的方式初始化一次就行。但是这么做会带来两个问题:
首先,UserCase
和UserCase2
分开初始化,意味着每次初始化都会去实例化一个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() 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.go
和wire.go
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport "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 package mainimport "github.com/google/wire" func wireApp () *UserCase2 { 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 package mainfunc wireApp () *UserCase2 { userCase := NewUserCase() userCase2 := NewUserCase2(userCase) return userCase2 }
这里细心的读者会发现,在wire.go
和wire_gen.go
中都生成了同样的一份函数wireApp
,不难猜到其实wire_gen.go
就是根据wire.go
中的代码生成的。从这里不难看出wire
根据我们提供的两个构造函数NewUserCase
和NewUserCase2
以及返回值*UserCase2
推断出了合理的初始化函数并生成。
目前这里看起来挺费事的,似乎和自己手写差不多。但是再次强调一下,对于大型项目来说,许多结构的初始化前置都隐含了几十个甚至上百个的前置依赖。这么想想,自动生成就省心多了。
另外说明一下,我们在wire.go
和wire_gen.go
开头看到的那些注释主要是为了在编译时忽略文件
这一对组合保证了两个文件任意情况下只有一个生效,从而避免文件内的标识符例如wireApp
出现重复定义的情况。
wire
的Provider和Injector这是wire
中最重要的两个概念,在实践了之后就比较容易懂了:
Provider:生成组件的普通方法。这些方法接收所需依赖作为参数,创建组件并将其返回。简单地说就是我们定义的NewUserCase
和NewUserCase2
。
Injector: 由wire自动生成的函数。函数内部会按根据依赖顺序调用相关privoder。也就是wire_gen.go
中生成的wireApp
函数。
此外,我们还可以将Provider包装起来成为一个ProviderSet来使用,这样在wire.go
中我们就可以简化书写。例如修改一下main.go
,增加一行:
1 var UCSet = wire.NewSet(NewUserCase, NewUserCase2)
然后将wire.go
的wireapp
修改成这样:
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 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() if err != nil { panic (err) } fmt.Println(uc2) cleanup() }
在上述代码中,UserCase
新建了一条tcp连接,并且返回了一个清理函数,用于关闭这条连接。 之后修改一下wireApp()
的实现:
1 2 3 4 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 ...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 }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 package mainimport ( "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, } }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:"-"` }
修改后的代码如下所示:
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 package mainimport ( "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 package mainfunc wireApp () *UserCase2 { userCase := &UserCase{} userCase2 := &UserCase2{ u: userCase, } return userCase2 }
注意到wire_gen.go
中操作了非公开访问的属性,这么做不会报错只是因为UserCase2
在main
包内。正常情况下使用这种属性绑定的方式就要求属性是公开访问的。便利性有了,代价是封装性下降。应该选用什么方式就留给大家自行权衡了。
使用结构体字段作为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) )