依赖注入(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 文件,它包含以下几个结构体:
1type Message string
2
3type Greeter struct {
4 Msg Message
5}
6
7type Event struct {
8 Greeter Greeter
9}
Message 表示消息,Greeter 表示问候者,它需要一个 Message,Event 表示一个事件,它用来触发问候。
现在,我们需要增加创建这些结构体的方法:
1func NewMessage(name string) Message {
2 return Message(fmt.Sprintf("hello, %s!", name))
3}
4
5func NewGreeter(msg Message) Greeter {
6 return Greeter{Msg: msg}
7}
8
9func NewEvent(g Greeter) Event {
10 return Event{Greeter: g}
11}
12
13func (g Greeter) Greet() Message {
14 return g.Msg
15}
16
17func (e Event) Start() {
18 msg := e.Greeter.Greet()
19 fmt.Println(msg)
20}
通过 NewMessage 创建消息,通过 NewGreeter 创建一个问候者,通过 NewEvent 创建事件,然后就可以调用 Start 方法来发起问候了,其实底层最终调用的是 Greeter 的 Greet 方法。
我们看看 main 方法如何使用它们:
1func main() {
2 msg := wire_demo.NewMessage("hank") // 创建msg
3 g := wire_demo.NewGreeter(msg) // 创建问候者
4 e := wire_demo.NewEvent(g) // 创建问候事件
5 e.Start()
6}
完成这个最普通示例,现在我们使用wire来改写上述实例。
使用wire改写程序
安装 wire
在使用wire前,我们需要先安装它,执行如下命令:
1go install github.com/google/wire/cmd/wire@latest
它会被安装到您的 $GOPATH/bin 目录中,因此您需要将它添加到环境变量 $PATH 中。
测试是否安装成功,执行 wire help 命令即可:
1➜ ~ wire help
2Usage: wire <flags> <subcommand> <subcommand args>
3
4Subcommands:
5 check print any Wire errors found
6 commands list all command names
7 diff output a diff between existing wire_gen.go files and what gen would generate
8 flags describe all known top-level flags
9 gen generate the wire_gen.go file for each package
10 help describe subcommands and their syntax
11 show describe all top-level provider sets
12
13
14Use "wire flags" for a list of top-level flags
输出上述帮助信息及表明安装成功。
改写程序
说是改写,其实我们根本不需要修改原来的 greeter.go ,要使用 wire 了,我们只需要在 go.mod 中添加其依赖包:
1require github.com/google/wire v0.5.0
然后,只需要再创建一个 wire.go 文件,代码如下:
1//go:build wireinject
2// +build wireinject
3
4package main
5
6import (
7 "github.com/google/wire"
8 "wire_demo")
9
10// 注入器
11func InitializeEvent(name string) wire_demo.Event {
12 wire.Build(wire_demo.NewEvent, wire_demo.NewGreeter, wire_demo.NewMessage)
13 return wire_demo.Event{}
14}
暂时先不去理会代码的逻辑,只需要知道这里我们编写了一个方法,该方法接收一个 name 参数,并调用了 wire.Build 方法,最后返回一个 Event 结构,稍后我们会详细解释。
生成注入代码
现在,我们需要调用 wire 命令来生成依赖注入的代码,前提是 wire 必须安装成功。在 wire.go
的同级目录下,我们执行如下命令:
1wire
就这么简单,程序会在当前目录下自动生成一个 wire_gen.go 文件,打开它,可以看到内容大致如下:
1// Code generated by Wire. DO NOT EDIT.
2
3//go:generate go run github.com/google/wire/cmd/wire//go:build !wireinject
4// +build !wireinject
5
6package main
7
8import (
9 "wire_demo"
10)
11
12// Injectors from wire.go:
13
14// 注入器
15func InitializeEvent(name string) wire_demo.Event {
16 message := wire_demo.NewMessage(name)
17 greeter := wire_demo.NewGreeter(message)
18 event := wire_demo.NewEvent(greeter)
19 return event
20}
这段代码是不是很熟悉?没错,这就是我们之前不使用依赖注入时的 main 方法的代码啊:
1func main() {
2 // 不使用依赖注入
3 msg := wire_demo.NewMessage("hank") // 创建msg
4 g := wire_demo.NewGreeter(msg) // 创建问候者
5 e := wire_demo.NewEvent(g) // 创建问候事件
6 e.Start()
7}
只是说现在我们编写 main 时需要调用 InitializeEvent 方法并传递所需的参数:
1func main() {
2 // 使用wire实现依赖注入
3 name := "jason"
4 e := InitializeEvent(name)
5 e.Start()
6}
运行的结果与之前不使用依赖注入时相同。
示例小结
通过上述示例,我们大致了解了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 代码如下:
1//go:build wireinject
2// +build wireinject
3
4package main
5
6import (
7 "github.com/google/wire"
8 "wire_demo")
9
10// 注入器
11func InitializeEvent(name string) wire_demo.Event {
12 //wire.Build(wire_demo.NewEvent, wire_demo.NewGreeter) // no provider found for wire_demo.Message
13 wire.Build(wire_demo.NewEvent, wire_demo.NewGreeter, wire_demo.NewMessage)
14 return wire_demo.Event{}
15}
//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,像这样:
1func initializeBaz(ctx context.Context) (foobarbaz.Baz, func(), error) {
2 panic(wire.Build(MegaSet))
3 return foobarbaz.Baz{}, func() {
4 }, nil
5}
注入器会返回最多三个参数(可以少于3个),第一个返回需要的对象;第二个为一个 func,用于清理资源,比如打开文件需要关闭、数据库连接需要关闭等等;第三个返回参数为 error,这是 wire 约定的返回格式,清理函数 func 或者 error 可以不返回。
另外,我们可以通过 wire.NewSet 方法将注入的资源归类,避免在 wire.Build 中写入过多的代码,比如上边的 MegaSet 就是这样,它组合了几个 Set:
1// SuperSet 使用NewSet创建集合
2var SuperSet = wire.NewSet(foobarbaz.ProvideFoo, foobarbaz.ProvideBar, foobarbaz.ProvideBaz)
3
4var OtherSet = wire.NewSet(foobarbaz.NewOther)
5
6// MegaSet 可以将其他集合加入新的集合中
7var MegaSet = wire.NewSet(SuperSet, OtherSet)
供应器
供应器(Provider)是程序提供的返回被注入对象的方法,比如前边示例的 NewMessage、NewGreeter 和 NewEvent 都是 Provider。Provider 会传递给 wire.Build 或者 wire.NewSet 方法,wire 通过分析各个Provider 来整理、填充对象图,最终自动生成完整的代码。
1func NewMessage(name string) Message {
2 return Message(fmt.Sprintf("hello, %s!", name))
3}
4
5func NewGreeter(msg Message) Greeter {
6 return Greeter{Msg: msg}
7}
8
9func NewEvent(g Greeter) Event {
10 return Event{Greeter: g}
11}
上边的三个 Provider 彼此存在依赖关系,当我们将其传递给 wire.Build 方法后,wire 能够自动分析它们之间的依赖关系,在实际使用中我们并不需要关心 Message 是怎么创建的。
wire 还有许多高级特性,比如绑定接口、注入结构体、绑定值、忽略结构体属性等等,有兴趣可以参阅官方文档。
本文示例代码见 github。
参考文档