依赖注入(Dependency injection)不是一个新概念了,早在2004年 Martin Fowler 就在其文章Inversion of Control Containers and the Dependency Injection pattern提出了依赖注入模式。在Java的世界中,随处可见依赖注入的使用,如Spring框架的核心之一就是控制反转(IoC),而依赖注入就是 IoC 思想的一种技术实现,比如我们常用的 Spring 的注解 @Autowire,还有 Jsr250规范定义的 @Resource 注解都实现了依赖注入。

简单而言,依赖注入就是以构建松散耦合的程序为目的、以控制反转为基本思想而在软件工程实现的一种编程技术,它使得程序不需要关注对象的创建逻辑,只是通过函数或者属性告诉程序自己需要什么对象,关注所依赖对象的使用逻辑,从而将对象的创建和使用过程分离开。可见,依赖注入技术遵循依赖倒置原则

虽然 GO 不是面向对象的语言,但是它也有依赖注入实现,常用的依赖注入框架包括 google 自身出品的 wireUber的digFacebook的inject等等。本文将介绍 wire 的基本使用。

不使用依赖注入时

在开始介绍 wire 之前,我们来编写一个简单的程序,该程序可以创建问候者并向您问好。

我们创建一个 greeter.go 文件,它包含以下几个结构体:

1
2
3
4
5
6
7
8
9
type Message string  
  
type Greeter struct {  
   Msg Message  
}  
  
type Event struct {  
   Greeter Greeter  
}

Message 表示消息,Greeter 表示问候者,它需要一个 MessageEvent 表示一个事件,它用来触发问候。

现在,我们需要增加创建这些结构体的方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func NewMessage(name string) Message {  
   return Message(fmt.Sprintf("hello, %s!", name))  
}
  
func NewGreeter(msg Message) Greeter {  
   return Greeter{Msg: msg}  
}  
  
func NewEvent(g Greeter) Event {  
   return Event{Greeter: g}  
}  
  
func (g Greeter) Greet() Message {  
   return g.Msg  
}  
  
func (e Event) Start() {  
   msg := e.Greeter.Greet()  
   fmt.Println(msg)  
}

通过 NewMessage 创建消息,通过 NewGreeter 创建一个问候者,通过 NewEvent 创建事件,然后就可以调用 Start 方法来发起问候了,其实底层最终调用的是 GreeterGreet 方法。

我们看看 main 方法如何使用它们:

1
2
3
4
5
6
func main() {  
   msg := wire_demo.NewMessage("hank") // 创建msg  
   g := wire_demo.NewGreeter(msg)      // 创建问候者  
   e := wire_demo.NewEvent(g)          // 创建问候事件  
   e.Start()  
}

完成这个最普通示例,现在我们使用wire来改写上述实例。

使用wire改写程序

安装 wire

在使用wire前,我们需要先安装它,执行如下命令:

1
go install github.com/google/wire/cmd/wire@latest

它会被安装到您的 $GOPATH/bin 目录中,因此您需要将它添加到环境变量 $PATH 中。

测试是否安装成功,执行 wire help 命令即可:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
  ~ wire help
Usage: wire <flags> <subcommand> <subcommand args>

Subcommands:
	check            print any Wire errors found
	commands         list all command names
	diff             output a diff between existing wire_gen.go files and what gen would generate
	flags            describe all known top-level flags
	gen              generate the wire_gen.go file for each package
	help             describe subcommands and their syntax
	show             describe all top-level provider sets


Use "wire flags" for a list of top-level flags

输出上述帮助信息及表明安装成功。

改写程序

说是改写,其实我们根本不需要修改原来的 greeter.go ,要使用 wire 了,我们只需要在 go.mod 中添加其依赖包:

1
require github.com/google/wire v0.5.0

然后,只需要再创建一个 wire.go 文件,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
//go:build wireinject  
// +build wireinject  
  
package main  
  
import (  
   "github.com/google/wire"  
   "wire_demo")  
  
// 注入器  
func InitializeEvent(name string) wire_demo.Event {  
   wire.Build(wire_demo.NewEvent, wire_demo.NewGreeter, wire_demo.NewMessage)  
   return wire_demo.Event{}  
}

暂时先不去理会代码的逻辑,只需要知道这里我们编写了一个方法,该方法接收一个 name 参数,并调用了 wire.Build 方法,最后返回一个 Event 结构,稍后我们会详细解释。

生成注入代码

现在,我们需要调用 wire 命令来生成依赖注入的代码,前提是 wire 必须安装成功。在 wire.go 的同级目录下,我们执行如下命令:

1
wire

就这么简单,程序会在当前目录下自动生成一个 wire_gen.go 文件,打开它,可以看到内容大致如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// Code generated by Wire. DO NOT EDIT.  
  
//go:generate go run github.com/google/wire/cmd/wire//go:build !wireinject  
// +build !wireinject  
  
package main  
  
import (  
   "wire_demo"  
)  
  
// Injectors from wire.go:  
  
// 注入器  
func InitializeEvent(name string) wire_demo.Event {  
   message := wire_demo.NewMessage(name)  
   greeter := wire_demo.NewGreeter(message)  
   event := wire_demo.NewEvent(greeter)  
   return event  
}

这段代码是不是很熟悉?没错,这就是我们之前不使用依赖注入时的 main 方法的代码啊:

1
2
3
4
5
6
7
func main() {  
   // 不使用依赖注入
   msg := wire_demo.NewMessage("hank") // 创建msg  
   g := wire_demo.NewGreeter(msg)      // 创建问候者  
   e := wire_demo.NewEvent(g)          // 创建问候事件  
   e.Start()  
}

只是说现在我们编写 main 时需要调用 InitializeEvent 方法并传递所需的参数:

1
2
3
4
5
6
func main() {  
   // 使用wire实现依赖注入  
   name := "jason"  
   e := InitializeEvent(name)  
   e.Start()  
}

运行的结果与之前不使用依赖注入时相同。

示例小结

通过上述示例,我们大致了解了wire的基本使用过程:

  1. 安装 wire
  2. 在项目中加入 wire 的依赖库
  3. 在项目 main 包中增加 wire.go 文件,用来调用 wire 编写注入方法
  4. 执行 wire 命令自动生成或者更新 wire_gen.go 文件
  5. 项目 main 方法调用 wire_gen.go 中的注入方法

整个过程还是非常简单的,但这与我们设想的依赖注入是不是不太一样呢?在 java 代码中,我们通常是通过在构造函数、私有属性或者 Setter 上标注 @Autowire 或者 @Resource 注解来实现依赖注入的,然后,wire 却不一样。

因为,wire 是通过 代码生成 来完成依赖注入的。java 或者文章开头提到的 diginject 库其实都是运行时依赖注入,通过反射等技术手段在运行时动态的实现依赖注入,但是这样的技术有一个缺点就是:运行时会降低性能。而 wire 则独辟蹊径,通过编译时分析对象依赖图自动生成依赖注入代码,从而实现在编译期间就完成依赖注入,不会影响程序的性能。

接下来,我们来详细了解wire。

wire详解

wire.go文件

wire.go 文件需要我们手动创建,并且在里边编码实现依赖注入的逻辑,由于注入入口在该文件中,并且 main 包需要使用它,因此,它必须位于 main 包中,与 main 方法同级。

实际上,wire.go 名称时约定俗成的,我们也可以随意给它取名字,但是很明显 wire.go 更便于理解。

前边实例中,wire.go 代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//go:build wireinject  
// +build wireinject  
  
package main  
  
import (  
   "github.com/google/wire"  
   "wire_demo")  
  
// 注入器  
func InitializeEvent(name string) wire_demo.Event {  
   //wire.Build(wire_demo.NewEvent, wire_demo.NewGreeter) //  no provider found for wire_demo.Message  
   wire.Build(wire_demo.NewEvent, wire_demo.NewGreeter, wire_demo.NewMessage)  
   return wire_demo.Event{}  
}

//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,像这样:

1
2
3
4
5
func initializeBaz(ctx context.Context) (foobarbaz.Baz, func(), error) {  
   panic(wire.Build(MegaSet))  
   return foobarbaz.Baz{}, func() {  
   }, nil  
}

注入器会返回最多三个参数(可以少于3个),第一个返回需要的对象;第二个为一个 func,用于清理资源,比如打开文件需要关闭、数据库连接需要关闭等等;第三个返回参数为 error,这是 wire 约定的返回格式,清理函数 func 或者 error 可以不返回。

另外,我们可以通过 wire.NewSet 方法将注入的资源归类,避免在 wire.Build 中写入过多的代码,比如上边的 MegaSet 就是这样,它组合了几个 Set

1
2
3
4
5
6
7
// SuperSet 使用NewSet创建集合  
var SuperSet = wire.NewSet(foobarbaz.ProvideFoo, foobarbaz.ProvideBar, foobarbaz.ProvideBaz)  
  
var OtherSet = wire.NewSet(foobarbaz.NewOther)  
  
// MegaSet 可以将其他集合加入新的集合中  
var MegaSet = wire.NewSet(SuperSet, OtherSet)

供应器

供应器(Provider)是程序提供的返回被注入对象的方法,比如前边示例的 NewMessageNewGreeterNewEvent 都是 Provider。Provider 会传递给 wire.Build 或者 wire.NewSet 方法,wire 通过分析各个Provider 来整理、填充对象图,最终自动生成完整的代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func NewMessage(name string) Message {  
   return Message(fmt.Sprintf("hello, %s!", name))  
}  
  
func NewGreeter(msg Message) Greeter {  
   return Greeter{Msg: msg}  
}  
  
func NewEvent(g Greeter) Event {  
   return Event{Greeter: g}  
}

上边的三个 Provider 彼此存在依赖关系,当我们将其传递给 wire.Build 方法后,wire 能够自动分析它们之间的依赖关系,在实际使用中我们并不需要关心 Message 是怎么创建的。

wire 还有许多高级特性,比如绑定接口、注入结构体、绑定值、忽略结构体属性等等,有兴趣可以参阅官方文档

本文示例代码见 github

参考文档


相关阅读