依赖注入(Dependency injection)不是一个新概念了,早在2004年 Martin Fowler 就在其文章Inversion of Control Containers and the Dependency Injection pattern提出了依赖注入模式。在Java的世界中,随处可见依赖注入的使用,如Spring框架的核心之一就是控制反转(IoC),而依赖注入就是 IoC 思想的一种技术实现,比如我们常用的 Spring 的注解 @Autowire
,还有 Jsr250规范定义的 @Resource
注解都实现了依赖注入。
简单而言,依赖注入就是以构建松散耦合的程序为目的、以控制反转为基本思想而在软件工程实现的一种编程技术,它使得程序不需要关注对象的创建逻辑,只是通过函数或者属性告诉程序自己需要什么对象,关注所依赖对象的使用逻辑,从而将对象的创建和使用过程分离开。可见,依赖注入技术遵循依赖倒置原则。
虽然 GO 不是面向对象的语言,但是它也有依赖注入实现,常用的依赖注入框架包括 google 自身出品的 wire
、Uber的dig、Facebook的inject等等。本文将介绍 wire 的基本使用。
不使用依赖注入时
在开始介绍 wire 之前,我们来编写一个简单的程序,该程序可以创建问候者并向您问好。
我们创建一个 greeter.go
文件,它包含以下几个结构体:
|
|
Message
表示消息,Greeter
表示问候者,它需要一个 Message
,Event
表示一个事件,它用来触发问候。
现在,我们需要增加创建这些结构体的方法:
|
|
通过 NewMessage
创建消息,通过 NewGreeter
创建一个问候者,通过 NewEvent
创建事件,然后就可以调用 Start
方法来发起问候了,其实底层最终调用的是 Greeter
的 Greet
方法。
我们看看 main
方法如何使用它们:
|
|
完成这个最普通示例,现在我们使用wire来改写上述实例。
使用wire改写程序
安装 wire
在使用wire前,我们需要先安装它,执行如下命令:
|
|
它会被安装到您的 $GOPATH/bin
目录中,因此您需要将它添加到环境变量 $PATH
中。
测试是否安装成功,执行 wire help
命令即可:
|
|
输出上述帮助信息及表明安装成功。
改写程序
说是改写,其实我们根本不需要修改原来的 greeter.go
,要使用 wire 了,我们只需要在 go.mod
中添加其依赖包:
|
|
然后,只需要再创建一个 wire.go
文件,代码如下:
|
|
暂时先不去理会代码的逻辑,只需要知道这里我们编写了一个方法,该方法接收一个 name
参数,并调用了 wire.Build
方法,最后返回一个 Event
结构,稍后我们会详细解释。
生成注入代码
现在,我们需要调用 wire
命令来生成依赖注入的代码,前提是 wire
必须安装成功。在 wire.go
的同级目录下,我们执行如下命令:
|
|
就这么简单,程序会在当前目录下自动生成一个 wire_gen.go
文件,打开它,可以看到内容大致如下:
|
|
这段代码是不是很熟悉?没错,这就是我们之前不使用依赖注入时的 main
方法的代码啊:
|
|
只是说现在我们编写 main
时需要调用 InitializeEvent
方法并传递所需的参数:
|
|
运行的结果与之前不使用依赖注入时相同。
示例小结
通过上述示例,我们大致了解了wire的基本使用过程:
- 安装 wire
- 在项目中加入 wire 的依赖库
- 在项目
main
包中增加wire.go
文件,用来调用 wire 编写注入方法 - 执行
wire
命令自动生成或者更新wire_gen.go
文件 - 项目
main
方法调用wire_gen.go
中的注入方法
整个过程还是非常简单的,但这与我们设想的依赖注入是不是不太一样呢?在 java 代码中,我们通常是通过在构造函数、私有属性或者 Setter 上标注 @Autowire
或者 @Resource
注解来实现依赖注入的,然后,wire 却不一样。
因为,wire 是通过 代码生成 来完成依赖注入的。java 或者文章开头提到的 dig
、inject
库其实都是运行时依赖注入,通过反射等技术手段在运行时动态的实现依赖注入,但是这样的技术有一个缺点就是:运行时会降低性能。而 wire 则独辟蹊径,通过编译时分析对象依赖图自动生成依赖注入代码,从而实现在编译期间就完成依赖注入,不会影响程序的性能。
接下来,我们来详细了解wire。
wire详解
wire.go文件
wire.go 文件需要我们手动创建,并且在里边编码实现依赖注入的逻辑,由于注入入口在该文件中,并且 main
包需要使用它,因此,它必须位于 main
包中,与 main
方法同级。
实际上,wire.go
名称时约定俗成的,我们也可以随意给它取名字,但是很明显 wire.go
更便于理解。
前边实例中,wire.go 代码如下:
|
|
//go:build wireinject
两行是 go 内部 build 指令,wireinject
表明使用 wire
命令注入时会根据该文件生成 wire_gen.go
文件,并且 wire.go
会被编译器忽略,如果该文件有编译错误则会在执行 wire
命令时暴露出来,IDE
无法自动提示编译错误信息,goland 中会给予警告:
注入器
注入器(Injector)是依赖注入的入口,也是需要我们手动编码实现依赖注入的部分。比如前边示例的 wire.go
中的 InitializeEvent
方法。
方法内部的 wire.Build
是wire核心方法,用来自动获取传入参数的依赖关系图,所有传入的参数wire都会分析其依赖图,从而自动填充依赖关系。
wire.Build
方法会返回一个错误消息,通常我们会直接调用 panic
,像这样:
|
|
注入器会返回最多三个参数(可以少于3个),第一个返回需要的对象;第二个为一个 func
,用于清理资源,比如打开文件需要关闭、数据库连接需要关闭等等;第三个返回参数为 error
,这是 wire 约定的返回格式,清理函数 func
或者 error
可以不返回。
另外,我们可以通过 wire.NewSet
方法将注入的资源归类,避免在 wire.Build
中写入过多的代码,比如上边的 MegaSet
就是这样,它组合了几个 Set
:
|
|
供应器
供应器(Provider)是程序提供的返回被注入对象的方法,比如前边示例的 NewMessage
、NewGreeter
和 NewEvent
都是 Provider。Provider 会传递给 wire.Build
或者 wire.NewSet
方法,wire 通过分析各个Provider 来整理、填充对象图,最终自动生成完整的代码。
|
|
上边的三个 Provider 彼此存在依赖关系,当我们将其传递给 wire.Build
方法后,wire
能够自动分析它们之间的依赖关系,在实际使用中我们并不需要关心 Message
是怎么创建的。
wire 还有许多高级特性,比如绑定接口、注入结构体、绑定值、忽略结构体属性等等,有兴趣可以参阅官方文档。
本文示例代码见 github。
参考文档