依赖注入(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。
参考文档