跳至主要內容

Wire: Go 的自动初始化工具

Mayee...大约 31 分钟

前言

在 JavaWeb 开发领域,Spring 家族早已是霸主地位,其开创式的提出了 IOC(Inversion of Control, 控制反转) 思想,并使用 DI(Dependency Injection, 依赖注入) 实现。 Spring 作为一框 Web 开发领域的框架,之所以叫做框架,是因为它包含了太多的功能,它可以管理对象的整个生命周期,并且提供了非常多的扩展点,为程序开发提供了极大地便利。 而今天要讲的 Wire 它只能叫做工具,因为它实际上就是一个代码生成工具,只不过它可以分析代码中的依赖关系,从而生成代码。

1. Wire 的介绍

wireopen in new window 是 Google 开源的一款依赖注入与代码生成工具,与 Go 隶属同宗。源码仓库中有大量的测试用例可供参考,同时有较为丰富的文档说明,入门新技术的最快方式就是看官方文档,好在它的文档篇幅较少,毕竟提供的功能有限,不像 Spring 都能出本书了。

Spring 中的依赖注入发生在运行时,也就是程序启动时,是自动的、动态的。程序启动时,我们定义的组件(component)会按照依赖顺序注册进 Spring 容器,当需要使用到某个组件时就可以从容器中取,非常方便。 不过也会有缺点,当注册的组件太多时,程序启动会很慢。 Wire 中的依赖注入发生在编译时,也就是编译代码的阶段,是手动的、静态的。我们需要事先使用wire命令生成代码,生成后的代码就是一段普通的代码,和我们自己写的差不多,只不过是生成的,省力、省心。这样的做的优点是不会对程序带来性能上的影响,缺点就是缺乏了灵活性。

Spring 中的依赖注入默认按照类型注入,它允许容器中存在多个相同类型的组件,如若如此,可进一步通过名称指定依赖组件;而 Wire 中只有类型注入,官方组织认为如果程序中存在相同类型的依赖,是程序设计上的缺陷。但如果就有那样的场景需要多个相同类型的依赖,在 wire 中该怎么做呢?比如,你的系统需要连接多个 Mysql 数据库,就需要在程序中定义多个数据源,这些数据源对象的类型可都是一样的。此时,我们可以通过 Go 的type关键字来定义新的类型即可,例如:type DSA DataSourcetype DSB DataSource...,则DSADSB在程序中就是不同的类型了。 Spring 中也允许循环依赖,在容器启动时通过三级缓存及延迟加载策略来实现,不过官方是不建议循环依赖的,因为循环依赖就是程序设计上的缺陷,所以 Spring 支持通过设置来禁止循环依赖,以严格的机制来推进程序质量的提升;而 Go 编译器从一开始就是禁止循环依赖的。

这两者相比算是各有千秋吧,JavaWeb 更多侧重于业务,一点点的性能损耗算是毛毛雨,而 Go 更多用于性能要求高但是业务轻的场景。语言没有高低之分,都有它适合的领域,在不同的业务场景下使用合适地解决方案才是应该去思考的。 wire 当前最新版本是v0.5.0,官方的态度是,这个工具已经很完备了,因此不会再加入新的功能,只会做一些故障修复。

2. Wire 的使用

在命令行工具中运行:go install github.com/google/wire/cmd/wire@latest,命令会安装在你的$GOPATH/bin下面,当然需要确保这个路径已经加入到$PATH中,才能够愉快地使用wire命令。 wire 中有两个核心概念:提供者(providers)和注入器(injectors)。

2.1 基础篇

2.1.1 基本用法

wire 的主要机制是提供者(provider): 一个可以产生(返回)出值的函数。 首先在main.go文件中定义出两个函数:NewBossNewCompany

package main

import "fmt"

type Boss struct {
	Message string
}

// NewBoss 返回一个 Boss 对象
func NewBoss() Boss {
	return Boss{Message: "I'm a Boss, hhh..."}
}

type Company struct {
	boss Boss
}

func (c Company) Declare() {
	fmt.Printf("declare from company: %s\n", c.boss.Message)
}

// NewCompany 返回一个 Company 对象,需要依赖一个 Boss 对象
func NewCompany(boss Boss) Company {
	return Company{boss: boss}
}

提供者函数名大写因此是可导出的,可以在其他包中使用,当然这并不是 wire 要求的。

接着在wire.go文件中定义一个普通函数:initCompany

//go:build wireinject

package main

import "github.com/google/wire"

func initCompany() Company {
	wire.Build(NewBoss, NewCompany)
	return Company{}
}

首先,来看initCompany函数,这是 Go 代码中再普通不过的函数了。函数名随意,是否可导出也随意,返回值更是随意。这都看你的实际需要,你想给函数起什么名字,想要函数返回什么值,只要编写的代码能通过 Go 的编译器就行。 在这里,initCompany函数就是注入器(injectors),NewBossNewCompany函数就是提供者(providers)。

不过这里也有一些需要注意的地方:

  1. 文件的开头写了//go:build wireinject,这是 Go 的条件编译,表示当编译命令中带有wireinject条件时,此文件会被编译。有些地方可能会写作// +build wireinject,也可能两种同时写。这两种写法的区别可参考Go工具命令open in new window。这个编译条件不是必须写在文件顶头,但必须在包名之前,并且必须和包名之间有空行,否则编译时会报错。
  2. initCompany函数的返回值无实际意义,只是为了能够被编译通过,即使在返回值中给Company对象赋值,在生成的代码中也会被忽略。

以下三种写法,生成的代码都是一样的,因此,根据实际情况以及个人喜好选择任一种写法即可。

// 写法一,也就是上面的写法
func initCompany() Company {
	wire.Build(NewBoss, NewCompany)
	return Company{}
}

// 写法二,当不想写无意义的返回值时这么写
func initCompany() Company {
	panic(wire.Build(NewBoss, NewCompany))
}

// 写法三,注意这里返回的是指针,如果你需要返回指针时可以这么写。 nil 可以被转换为指针,但不能被转换为结构体
func initCompany() *Company {
	wire.Build(NewBoss, NewCompany)
	return nil
}

有时候第二种写法会生成失败,提示missing return,关于这个问题,我向开源组织提交了 issueopen in new window 。 此时将 panic 中的参数换行写即可解决,如下:

func initBar() Bar {
	panic(
		wire.Build(NewBoss, NewCompany),
	)
}

现在,我们可以在wire.go文件同级的地方执行wire命令,当然可以在任何地方执行wire命令,但需要指定wire.go文件的路径。接着就会自动在同级目录下生成一个wire_gen.go文件:

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

// Injectors from wire.go:

func initCompany() Company {
	boss := NewBoss()
	company := NewCompany(boss)
	return company
}

在这个文件中,首行提示说,这是 wire 生成的代码,不要编辑。 当然不是说不能编辑这个文件,而是最好不要编辑。这就是一个普通的 Go 代码文件,和我们自己写的无任何差异,但是这个文件中的内容会在下一次执行wire命令时被覆盖。

接着是//go:generate go run github.com/google/wire/cmd/wire,这是 Go 的工具命令,主要是为方便在没有安装wire的环境中使用,在命令行中执行go generate就可以在本地没有安装wire命令的情况下生成wire_gen.go文件。

再下面两行,我们可以看到编译条件是!wireinject,和wire,go文件中的编译条件是互补的。当我们编译代码时不带wireinject指令则wire_gen.go文件会参与编译,wire.go文件不参与编译。也就是说wire.go文件只是一个模板文件,作用是为了生成wire_gen.go文件,程序启动时实际是用到wire_gen.go文件。而wire_gen.go文件中只有一个普通的函数initCompany返回了一个Company对象。 生成的这段代码就是一段普通的代码,我们也可以手写不用wire。这里是为了演示做了一个简单的示例。试想一下,如果在我们的程序中,需要的对象很多,依赖关系错综复杂,如果去手写这段代码将会非常痛苦且容易出错,如果后面修改了对象的依赖关系,还得修改这里的代码,简直苦不堪言。那么这个时候wire的作用就凸显出来的,只需要组织好个对象的依赖关系即可,剩下的就都交给wire吧。

通过上面这个简单的例子,已经可以初步感受到wire的魅力了,不必觉得它有多神秘,它不过就是你用来生成代码的工具,本文就是让我们能够全面了解它,并随心所欲的使用它。和 Spring 相比,wire 只是实现了 DI,没有实现 IOC。 所以,在wire.go文件中,你也可以定义多个函数,在wire_gen.go文件中都会生成对应的函数。

注意:注入器中需要的提供者函数不能缺少,否则肯定会提示缺少依赖。但提供者函数也不能过多,否则会提示unused provider。因此提供者函数必须不多不少正正好才行,这也符合 Go 语言的设计哲学。

2.1.2 返回 error

生成的注入器函数都是普通的 Go 代码,由于 Go 支持函数返回多个值,因此提供者函数也是可以产生多个值的,当然也可以返回error。此时,修改我们的main.go文件如下:

package main

import (
	"errors"
	"fmt"
	"github.com/google/wire"
)

// Staff 员工
type Staff struct {
	Rank int // 职级
}

func NewStaff() *Staff {
	return &Staff{Rank: 9} // 九品芝麻职员
}

type Company struct {
	staff *Staff
}

func NewCompany(staff *Staff) (*Company, error) {
	// 五品以下职员不能入朝
	if staff.Rank > 5 {
		return nil, errors.New("rank less than 5 can't enter the company")
	}
	return &Company{staff: staff}, nil
}

// MeetInCourt 朝见
func (c *Company) MeetInCourt() {
	fmt.Println("we all love each one")
}

var ProviderSet = wire.NewSet(
	NewStaff,
	NewCompany,
)

修改wire.go文件如下:

//go:build wireinject

package main

import "github.com/google/wire"

func initCompany() (*Company, error) {
	panic(
		wire.Build(ProviderSet),
	)
}

然后执行wire命令生成wire_gen.go文件如下:

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

// Injectors from wire.go:

func initCompany() (*Company, error) {
	staff := NewStaff()
	company, err := NewCompany(staff)
	if err != nil {
		return nil, err
	}
	return company, nil
}

首先,我们修改了NewCompany提供者函数,使其多返回了一个error值,那么对应的在wire.go文件中,initCompany注入器函数的返回值也要有error值,并且必须是(*Company, error)这样的顺序。 此外,我们定义了一个ProviderSet类型的变量,程序中也定义多个ProviderSet类型的变量,便于将提供这函数分组。如果多个提供者需要被频繁地组织在一些使用,这样做是非常好用的。

注意:不能在多个ProviderSet中传入相同的提供者函数,这是不允许的。此外,因为这里是改变了注入器函数的返回值,如果在重新生成前已经在程序中有调用initCompany函数了,那么wire生成时就会报错。因为 wire 在生成代码时会先编译你的程序,如果程序编译都无法通过,那么 wire 肯定是无法生成代码的,所以必须确保程序可以被正常编译。

2.2 高级篇

在基础篇中我们已经学会了 wire 的基本用法,接下来将介绍 wire 的高级用法。

2.2.1 接口绑定类型

通常,依赖注入是用于绑定接口的具体实现。在编写代码时提倡面向接口编程,这一点在使用 Spring 开发中尤为突出,通常@AutoWired的注解是作用在接口之上的,Spring 框架会自动为接口绑定它的实现类对象,这样一来,当我们在程序中为框架提供不同的实现类时,系统某些功能表现上就有所不同。使用接口来降低程序之间的耦合,是程序设计的常用做法。而@AutoWired的注解的对象,对应在 wire 中就是 Providers,即我们的提供这函数需要返回一个接口类型。 然而,这个习惯并不是 Go 提倡的最佳编程实践,Go 更提倡返回具体类型open in new window。那么在 wire 中,既想要提供者返回接口类型,又想要符合 Go 的实践返回具体类型,就需要为接口绑定具体类型了。

创建main.go文件内容如下:

package main

import (
	"fmt"
	"github.com/google/wire"
)

// Member 定义成员接口
type Member interface {
	Say() string
}

// Boss 老板是公司成员
type Boss struct {
	message string
}

// NewBoss Boss 提供者函数
func NewBoss() *Boss {
	return &Boss{message: "I'm a Boss, hhh..."}
}

// Say 实现成员接口
func (b *Boss) Say() string {
	return b.message
}

// Staff 普通职员也是公司成员
type Staff struct {
	message string
}

// NewStaff Staff 提供者函数
func NewStaff() *Staff {
	return &Staff{message: "I'm a Staff, www..."}
}

// Say 实现成员接口
func (s *Staff) Say() string {
	return s.message
}

// Company 公司中有一个成员
type Company struct {
	member Member // 可以是任何实现了成员接口的对象
}

// NewCompany Company 提供者函数,需要一个成员依赖
func NewCompany(member Member) *Company {
	return &Company{member: member}
}

// Declare 对外宣布
func (c *Company) Declare() {
	fmt.Printf("declare from company: %s\n", c.member.Say())
}

var ProviderSet = wire.NewSet(
	NewStaff,
	wire.Bind(new(Member), new(*Staff)),
	NewCompany,
)

创建wire.go文件内容如下:

//go:build wireinject

package main

import "github.com/google/wire"

func initCompany() *Company {
	panic(
		wire.Build(ProviderSet),
	)
}

然后执行wire命令生成wire_gen.go文件如下:

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

// Injectors from wire.go:

func initCompany() *Company {
	staff := NewStaff()
	company := NewCompany(staff)
	return company
}

这里我们定义了一个Member接口,以及两个实现类:StaffBoss。 在 ProviderSet 中有一个NewCompany提供者函数,它需要一个Member接口的成员依赖。而wire.Bind(new(Member), new(*Staff))即是提供了一个Member接口对象,同时为接口绑定了具体类型Staff,那么相应的就需要有一个Staff的提供者函数NewStaff。 关于wire.Bind(iface, to interface{})方法参数说明:

  • iface的值是一个指向了所需接口的指针。这里我们所需的接口是Member,获取它的指针用new(Member)
  • to的值是一个实现了所需接口类型的指针。这里我们绑定的类型是Staff,需要注意的是,由于NewStaff函数的返回值是*Staff,所以需要获取的是*Staff的指针用new(*Staff)

如果我们想将接口绑定的类型更换为Boss,那么需要修改 ProviderSet 中的值如下:

var ProviderSet = wire.NewSet(
	NewBoss,
	wire.Bind(new(Member), new(*Boss)),
	NewCompany,
)

然后再次执行wire命令,重新生成wire_gen.go文件:

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

// Injectors from wire.go:

func initCompany() *Company {
	boss := NewBoss()
	company := NewCompany(boss)
	return company
}

2.2.2 提供者构建结构体

可以使用提供者函数来构建结构体。使用wire.Struct()函数可以构建一个结构体,并且告诉注入器函数,结构体的哪些字段是需要被注入的。注入器函数将会按照结构体的各个字段类型来注入。

创建main.go文件内容如下:

package main

import (
	"fmt"
	"github.com/google/wire"
)

// Boss 老板是公司成员
type Boss struct {
}

// NewBoss Boss 提供者函数
func NewBoss() *Boss {
	return &Boss{}
}

// Staff 普通职员也是公司成员
type Staff struct {
}

// NewStaff Staff 提供者函数
func NewStaff() *Staff {
	return &Staff{}
}

// Company 公司中有一个老板和一个员工
type Company struct {
	boss  *Boss
	staff *Staff
}

// Declare 对外宣布
func (c *Company) Declare() {
	fmt.Printf("company have a boss: %t\n", c.boss != nil)
	fmt.Printf("company have a staff: %t\n", c.staff != nil)
}

var ProviderSet = wire.NewSet(
	NewBoss,
	NewStaff,
	wire.Struct(new(Company), "boss", "staff"),
)

创建wire.go文件内容如下:

//go:build wireinject

package main

import "github.com/google/wire"

func initCompany() *Company {
	panic(
		wire.Build(ProviderSet),
	)
}

然后执行wire命令生成wire_gen.go文件如下:

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

// Injectors from wire.go:

func initCompany() *Company {
	boss := NewBoss()
	staff := NewStaff()
	company := &Company{
		boss:  boss,
		staff: staff,
	}
	return company
}

这里我们定义了一个Company,其中包含两个字段:*Boss*Staff。 在ProviderSet中不在需要NewCompany提供者函数,而是使用wire.Struct(new(Company), "boss", "staff")来代替。我们指定了需要注入的两个字段名称bossstaff,那么相应的就需要有这两个提供者函数NewBossNewStaff。 关于Struct(structType interface{}, fieldNames ...string)方法参数说明:

  • structType的值是一个指向了所需结构体的指针。这里我们所需结构体是Company,获取它的指针用new(Company)
  • fieldNames的值是结构体的字段名,需要注意名称大写小,名称必须一致才行。写了哪些字段名,就会注入哪些字段,如果需要注入全部字段也可用"*"来简化写法;

如果initCompany函数返回的是Company

//go:build wireinject

package main

import "github.com/google/wire"

func initCompany() Company {
	panic(
		wire.Build(ProviderSet),
	)
}

那么生成的wire_gen.go文件如下:

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

// Injectors from wire.go:

func initCompany() Company {
	boss := NewBoss()
	staff := NewStaff()
	company := Company{
		boss:  boss,
		staff: staff,
	}
	return company
}

有时候我们简化写为:wire.Struct(new(Company), "*"),但又想要阻止某些字段被注入,可以给不想被注入的字段加上标签`wire:"-"`,如下:

type Company struct {
	boss  *Boss
	staff *Staff `wire:"-"`
}

然后执行wire命令重新生成wire_gen.go文件如下:

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

// Injectors from wire.go:

func initCompany() Company {
	boss := NewBoss()
	company := Company{
		boss: boss,
	}
	return company
}

如果此时在wire.Struct(new(Company), "staff")中指定注入staffwire命令会报错。

2.2.3 绑定值

有时候,需要将一个基本类型值(通常是 nil)绑定到接口,而不必为这种一次性的事情创建一个提供者函数,可以使用wire.Value()wire.InterfaceValue()ProviderSet添加值表达式。

创建main.go文件内容如下:

package main

import (
	"fmt"
	"github.com/google/wire"
)

type Member interface {
	Say() string
}

// Boss 老板是公司成员
type Boss struct {
	message string
}

// Staff 普通职员也是公司成员
type Staff struct {
	message string
}

func (s *Staff) Say() string {
	return s.message
}

type Company struct {
	boss   *Boss
	member Member
}

// Declare 对外宣布
func (c *Company) Declare() {
	fmt.Println(c.boss.message)
	fmt.Println(c.member.Say())
}

var ProviderSet = wire.NewSet(
	wire.Value(&Boss{message: "I'm a Boss, hhh..."}),
	wire.InterfaceValue(new(Member), &Staff{message: "I'm a Staff, www..."}),
	wire.Struct(new(Company), "*"),
)

创建wire.go文件内容如下:

//go:build wireinject

package main

import "github.com/google/wire"

func initCompany() Company {
	panic(
		wire.Build(ProviderSet),
	)
}

然后执行wire命令生成wire_gen.go文件如下:

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

// Injectors from wire.go:

func initCompany() Company {
	boss := _wireBossValue
	member := _wireStaffValue
	company := Company{
		boss:   boss,
		member: member,
	}
	return company
}

var (
	_wireBossValue  = &Boss{message: "I'm a Boss, hhh..."}
	_wireStaffValue = &Staff{message: "I'm a Staff, www..."}
)

这里我们定义了一个Company,其中包含了两个字段:*BossMemeber。其中Member是一个接口,Staff实现了这个接口。 我们在main.go中没有定义任何NewXXX提供者函数,而是使用了wire.Value()wire.InterfaceValue()wire.Struct()这三个函数完成了依赖注入。 wire.Struct()我们在上一节中已经说过,这里主要说wire.Value()wire.InterfaceValue()这两个函数。 我们回忆一下,在没有使用wire.Value()函数的时候,我们需要定义了一个NewBoss提供者函数,因此wire.Value()函数的作用就是可以省略定义这种一次性的提供者函数。 然后是在没有使用wire.InterfaceValue()函数的时候,我们需要定义一个NewStaff提供者函数,以及在ProviderSet中写wire.Bind(new(Member),new(*Staff)),因此wire.InterfaceValue()作用就是代替了前面这两处。 简单来说,wire.Value()wire.InterfaceValue()更像是提供的一种语法糖函数。

注意,wire.Value()中的参数不能是接口,如果提供的是指针类型,则需要写作&Boss{},若写作new(Boss)会报错,提示:argument to Value is too complex

2.2.4 结构体字段作为提供者

有时候,我们想把结构体中的字段值作为提供者,不必定义一个GetXXX提供者函数将结构体中的字段暴露出来,而是使用wire.FieldsOf()函数。

创建main.go文件内容如下:

package main

import (
	"fmt"
	"github.com/google/wire"
)

// Competitor 竞争对手公司
type Competitor struct {
	staff *Staff
}

// Staff 普通职员
type Staff struct {
	name string
}

type Company struct {
	staff *Staff
}

// Declare 对外宣布
func (c *Company) Declare() {
	fmt.Printf("We hired %s from a competitor\n", c.staff.name)
}

var ProviderSet = wire.NewSet(
	wire.Value(Competitor{staff: &Staff{name: "bigOld"}}), // 竞争公司里有一位大佬
	wire.FieldsOf(new(Competitor), "staff"),               // 竞争公司的大佬暴露出来了
	wire.Struct(new(Company), "staff"),                    // 把大佬注入到本公司
)

创建wire.go文件内容如下:

//go:build wireinject

package main

import "github.com/google/wire"

func initCompany() *Company {
	panic(
		wire.Build(ProviderSet),
	)
}

然后执行wire命令生成wire_gen.go文件如下:

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

// Injectors from wire.go:

func initCompany() *Company {
	competitor := _wireCompetitorValue
	staff := competitor.staff
	company := &Company{
		staff: staff,
	}
	return company
}

var (
	_wireCompetitorValue = Competitor{staff: &Staff{name: "bigOld"}}
)

这里我们先是使用wire.Value得到一个Competitor提供者,接着使用wire.FieldsOfCompetitor中的staff字段值作为一个提供者,最后使用wire.Struct注入了staff字段。 同wire.Struct类似,wire.FieldsOf也可以将多个字段作为提供者。

2.2.5 清理函数

有时候,提供者在返回值的同时,可能需要有清理数据、关闭资源等操作,就会需要返回一个闭包函数来处理。例如,数据库连接池关闭资源、文件对象关闭资源等操作,当程序中发生错误时,需要调用清理函数执行相应的动作。

创建main.go文件内容如下:

package main

import (
	"errors"
	"fmt"
	"github.com/google/wire"
)

// Staff 普通职员
type Staff struct {
	rank int
}

func NewStaff() (*Staff, func()) {
	return &Staff{rank: 9}, func() {
		fmt.Println("cleanup in Staff")
	}
}

// Manager 经理
type Manager struct {
	staff *Staff
}

func NewManager(staff *Staff) (*Manager, func()) {
	return &Manager{staff: staff}, func() {
		fmt.Println("cleanup in Manager")
	}
}

// Boss 老板
type Boss struct {
	manager *Manager
}

func NewBoss(manager *Manager) (*Boss, func()) {
	return &Boss{manager: manager}, func() {
		fmt.Println("cleanup in Boss")
	}
}

type Company struct {
	boss *Boss
}

func NewCompany(boss *Boss) (*Company, error) {
	if boss.manager.staff.rank > 5 {
		return nil, errors.New("rank less than 5 can't enter the company")
	}
	return &Company{boss: boss}, nil
}

// MeetInCourt 朝见
func (c *Company) MeetInCourt() {
	fmt.Println("we all love each one")
}

var ProviderSet = wire.NewSet(
	NewStaff,
	NewManager,
	NewBoss,
	NewCompany,
)

创建wire.go文件内容如下:

//go:build wireinject

package main

import "github.com/google/wire"

func initCompany() (*Company, func(), error) {
	panic(
		wire.Build(ProviderSet),
	)
}

然后执行wire命令生成wire_gen.go文件如下:

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

// Injectors from wire.go:

func initCompany() (*Company, func(), error) {
	staff, cleanup := NewStaff()
	manager, cleanup2 := NewManager(staff)
	boss, cleanup3 := NewBoss(manager)
	company, err := NewCompany(boss)
	if err != nil {
		cleanup3()
		cleanup2()
		cleanup()
		return nil, nil, err
	}
	return company, func() {
		cleanup3()
		cleanup2()
		cleanup()
	}, nil
}

这里我们定义了三个类型:StaffManagerBoss,它们三个层层依赖,并且各自都有清理函数。而Company依赖了Boss并且还会返回一个error。 那么,在我们的注入器函数initCompany中就需要返回(*Company, func(), error)。即任何一个提供者函数返回了清理函数,则注入器函数必需要返回一个清理函数;即任何一个提供者函数返回了error,则注入器函数必需要返回一个error。 所以,注入器函数initCompany的返回值可能有四种情况:

  1. 返回 *Company ;
  2. 返回 (*Company, error) ;
  3. 返回 (*Company, func()) ;
  4. 返回 (*Company, func(), error) ;

注意,当注入器函数返回多值时,一定要按照顺序定义,否则wire命令报错。

wire_gen.go文件中我们可以看到,当注入器中发生error时,会自动调用清理函数。当多个提供者都有清理函数时,会将其聚合,最终会按照对象的依赖层级,从外层对象的清理函数到内层对象的清理函数,依次执行。

3. wire 最佳实践

3.1 可区分的类型

有时候,我们需要注入一个基本类型如string,就需要为string创建一个新的类型来避免与其他提供者发生冲突。

创建main.go文件内容如下:

package main

import (
	"fmt"
	"github.com/google/wire"
)

type BossName string
type StaffName string

type Boss struct {
	name BossName
}

type Staff struct {
	name StaffName
}

type Company struct {
	boss  Boss
	staff Staff
}

func (c Company) Declare() {
	fmt.Printf("company of the boss name: %s\n", c.boss.name)
	fmt.Printf("company of the staff name: %s\n", c.staff.name)
}

var Provider = wire.NewSet(
	wire.Value(BossName("bobby")),
	wire.Value(StaffName("regan")),
	wire.Struct(new(Boss), "*"),
	wire.Struct(new(Staff), "*"),
	wire.Struct(new(Company), "*"),
)

创建wire.go文件内容如下:

//go:build wireinject

package main

import "github.com/google/wire"

func initCompany() *Company {
	panic(
		wire.Build(Provider),
	)
}

然后执行wire命令生成wire_gen.go文件如下:

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

// Injectors from wire.go:

func initCompany() *Company {
	bossName := _wireBossNameValue
	boss := Boss{
		name: bossName,
	}
	staffName := _wireStaffNameValue
	staff := Staff{
		name: staffName,
	}
	company := &Company{
		boss:  boss,
		staff: staff,
	}
	return company
}

var (
	_wireBossNameValue  = BossName("bobby")
	_wireStaffNameValue = StaffName("regan")
)

这里我们定义了StaffBoss两个对象,其中都含有一个name属性,并且属性类型相同。如果直接将name的类型定义为string,则在执行wire命令时会报错。 前面我们说过,wire是靠类型注入的,相同的类型则会导致冲突而无法完成注入,所以我们需要重新定义string来区分类型,而BossNameStaffName虽然底层类型都是string,但实际上他们是不同的类型(type),因此可以正常完成注入。

3.2 可选项结构体

有时候,一个提供者函数中包含了比较多的依赖项,则可以创建一个可选项结构体与这个提供者函数配对。

创建main.go文件内容如下:

package main

import (
	"fmt"
	"github.com/google/wire"
)

// Reception 前台
type Reception struct {
}

// Finance 财务
type Finance struct {
}

// Personnel 人事
type Personnel struct {
}

// Manager 经理
type Manager struct {
}

// Boss 老板
type Boss struct {
}

type CompanyOptions struct {
	boss      *Boss
	manager   *Manager
	reception *Reception `wire:"-"` // 前台就不用了,现在是小公司,人事兼任一下接待就行
	finance   *Finance
	personnel *Personnel
}

type Company struct {
	opts *CompanyOptions
}

func (c *Company) Declare() {
	fmt.Printf("the company have Boss: %t\n", c.opts.boss != nil)
	fmt.Printf("the company have Manager: %t\n", c.opts.manager != nil)
	fmt.Printf("the company have Reception: %t\n", c.opts.reception != nil)
	fmt.Printf("the company have Finance: %t\n", c.opts.finance != nil)
	fmt.Printf("the company have Personnel: %t\n", c.opts.personnel != nil)
}

var Provider = wire.NewSet(
	wire.Value(&Boss{}),      // 公司必须得有老板,老板出去讲故事拉融资才能有钱
	wire.Value(&Finance{}),   // 财务也必须有,不然没处领工资
	wire.Value(&Personnel{}), // 人事得有,要招聘靠谱的员工才能把公司做大做强
	wire.Value(&Manager{}),   // 经理得有,不然什么事都得老板操心,公司迟早玩完
	wire.Struct(new(CompanyOptions), "*"),
	wire.Struct(new(Company), "opts"),
)

创建wire.go文件内容如下:

//go:build wireinject

package main

import "github.com/google/wire"

func initCompany() *Company {
	panic(
		wire.Build(Provider),
	)
}

然后执行wire命令生成wire_gen.go文件如下:

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

// Injectors from wire.go:

func initCompany() *Company {
	boss := _wireBossValue
	manager := _wireManagerValue
	finance := _wireFinanceValue
	personnel := _wirePersonnelValue
	companyOptions := &CompanyOptions{
		boss:      boss,
		manager:   manager,
		finance:   finance,
		personnel: personnel,
	}
	company := &Company{
		opts: companyOptions,
	}
	return company
}

var (
	_wireBossValue      = &Boss{}
	_wireManagerValue   = &Manager{}
	_wireFinanceValue   = &Finance{}
	_wirePersonnelValue = &Personnel{}
)

这里我们本意是想构建一个对象(Company),而Company中有若干个字段(即岗位),有些字段(岗位)在初期没有,但在后续(随着公司的发展),将会增加新的字段(岗位),那就需要对Company进行修改,这种修改有较大的侵入性,当我们的系统较为庞大时,构建的这个Company对象将会很复杂,这种做法很不利于维护。 因此,我们创建一个CompanyOptions对象,即通过一个中间对象将Company与其中的属性做了解耦,提升了Company的可维护性。

3.3 在库中的提供者集合

有时候,我们会在库(即Library,指从外部引入的三方库,如wire、gin、gorm等)中创建提供者集合(ProviderSet)便于开发者使用。当我们需要更改ProviderSet时,只有以下操作才能不破坏兼容性:

  • 更改ProviderSet中的一个提供者函数的实现,但不能更改这个提供者函数的输出类型,并且更改后一定要使用wire命令重新生成代码才能生效。
  • ProviderSet中引入一个新的提供者,这个提供者输出的类型(type)必须是新增(即对一个已存在的类型 type 创建新的类型)的,这个新类型需要是之前都不存在的。否则很可能在注入器中已经包含了这个输出类型,就会造成冲突,导致wire命令生成代码会失败。

所有的这些更改都是不安全的:

  • ProviderSet中的提供者函数需要依赖一个新的输入类型;
  • ProviderSet中移除了一个输出类型(即移除了一个提供者);
  • ProviderSet中增加了一个输入类型(即新增了一个提供者),而这个类型是已存在的;

因此,在库中对外提供的ProviderSet中,所有提供者所产生的输出类型一定要仔细挑选。一般来说,库中对外提供的ProviderSet中的提供者数量尽可能地少,并且提供者函数需要依赖的输入,以及其产生的输出,也要尽可能地少。通常,在库中对外提供的ProviderSet中只包含单个提供者,这个提供者返回接口类型,并且使用wire.Bind()来绑定具体实现类型。避免创建一个较大的ProviderSet,以降低程序中发生冲突的可能性。 为了说明这个问题,试着想象一下,在你库中的ProviderSet提供了一个用于 Web Api 服务的客户端(即提供者函数),这个客户端当然需要依赖一个*http.Client以发送 HTTP 请求。如果你在ProviderSet中同时也提供了一个*http.Client作为客户端(提供者函数)的输入(参数),这样做似乎挺好的,可是如果每个库都这样做呢?多个ProviderSet都提供了*http.Client就会造成冲突。因此,在你库中的ProviderSet应该只包含客户端,让*http.Client从外部提供,作为注入器函数的输入。

3.4 wire mock

有时候,我们想要对某些依赖进行 mock,在 Spring 中是很方便的,但在 wire 中怎么做呢?mock 的思想一般是依赖接口,替换不同的实现可达到 mock 效果,在 Java 中花样比较多,有通过反射实现的,通过字节码插桩的,通过类加载替换的。因为语言机制的不同,Java 在程序编译到运行的各个时期都可以做文章,实际上 Go 也可以在编译期到运行期做文章,只是感觉花样比 Java 少。 Go 没有类似 Java 的类加载过程,一般是通过代码编译、反射机制来实现 mock。而 Go 的反射机制不似 Java 那般强大,且 Go 的反射会带来很大的性能损耗。老实说,使用 Go 如果不注重性能的话,那和用 Java 又有什么分别呢。

wire mock 有两种方式:

  • 方式 A:传入 mock 给注入器。创建一个测试专用的注入器函数,将所有的 mock 变量都作为注入器函数的参数,这些变量的类型必须是正在 mock 对象的接口类型。wire.Build中不包含返回 mock 类型的提供者函数就不会造成冲突,所以就需要定义一个不包含 mock 类型的ProviderSet
  • 方式 B:注入器返回 mock。创建一个测试专用的注入器函数,这个函数返回一个新的结构类型,这个结构中包含原本需要的结构以及所需的 mock 提供者,并使用wire.Bind将 mock 结构与接口绑定。

下面使用示例代码来说明。

3.4.1 方式 A

创建main.go文件内容如下:

package main

import (
	"fmt"
	"github.com/google/wire"
)

// Boss 老板接口
type Boss interface {
	Say() string // 老板宣言
}

// RealBoss 真老板
type RealBoss struct {
	message string
}

func (r *RealBoss) Say() string {
	return r.message
}

// FakerBoss 假老板
type FakerBoss struct {
	message string
}

func (f *FakerBoss) Say() string {
	return f.message
}

type Company struct {
	boss Boss
}

func (c *Company) Declare() {
	fmt.Printf("the company Boss declare: %s\n", c.boss.Say())
}

var ProviderBoss = wire.NewSet(
	wire.InterfaceValue(new(Boss), &RealBoss{message: "I'm real boss, I have 300 small goals"}),
)

var ProviderCompany = wire.NewSet(
	wire.Struct(new(Company), "*"),
)

创建wire.go文件内容如下:

//go:build wireinject

package main

import "github.com/google/wire"

func initCompany() *Company {
	panic(
		wire.Build(ProviderBoss, ProviderCompany),
	)
}

func initMockCompany(boss Boss) *Company {
	panic(
		wire.Build(ProviderCompany),
	)
}

然后执行wire命令生成wire_gen.go文件如下:

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

// Injectors from wire.go:

func initCompany() *Company {
	boss := _wireRealBossValue
	company := &Company{
		boss: boss,
	}
	return company
}

var (
	_wireRealBossValue = &RealBoss{message: "I'm real boss, I have 300 small goals"}
)

func initMockCompany(boss Boss) *Company {
	company := &Company{
		boss: boss,
	}
	return company
}

这里我们创建了两个注入器函数initCompanyinitMockCompany,它们有相同的返回值*Company,在Company结构中持有一个Boss接口类型的对象。不同的是initMockCompany函数需要从外部传入这个Boss接口对象,因此在使用中我们传入一个*FakerBoss,便达到了 mock 的效果。

3.4.2 方式 B

创建main.go文件内容如下:

package main

import (
	"fmt"
	"github.com/google/wire"
)

// Boss 老板接口
type Boss interface {
	Say() string // 老板宣言
}

// RealBoss 真老板
type RealBoss struct {
	message string
}

func (r *RealBoss) Say() string {
	return r.message
}

// FakerBoss 假老板
type FakerBoss struct {
	message string
}

func (f *FakerBoss) Say() string {
	return f.message
}

type Company struct {
	boss Boss
}

func (c *Company) Declare() {
	fmt.Printf("the company Boss declare: %s\n", c.boss.Say())
}

// MockCompany 假公司
type MockCompany struct {
	company   *Company
	fakerBoss *FakerBoss
}

func (m *MockCompany) Declare() {
	fmt.Printf("the company Boss declare: %s\n", m.company.boss.Say())
}

var ProviderCompany = wire.NewSet(
	wire.InterfaceValue(new(Boss), &RealBoss{message: "I'm real boss, I have 300 small goals"}),
	wire.Struct(new(Company), "*"),
)

var ProviderMockCompany = wire.NewSet(
	wire.Value(&FakerBoss{message: "I'm faker boss, I owe 300 small goals"}),
	wire.Bind(new(Boss), new(*FakerBoss)),
	wire.Struct(new(Company), "*"),
	wire.Struct(new(MockCompany), "*"),
)

创建wire.go文件内容如下:

//go:build wireinject

package main

import "github.com/google/wire"

func initCompany() *Company {
	panic(
		wire.Build(ProviderCompany),
	)
}

func initMockCompany() *MockCompany {
	panic(
		wire.Build(ProviderMockCompany),
	)
}

然后执行wire命令生成wire_gen.go文件如下:

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

// Injectors from wire.go:

func initCompany() *Company {
	boss := _wireRealBossValue
	company := &Company{
		boss: boss,
	}
	return company
}

var (
	_wireRealBossValue = &RealBoss{message: "I'm real boss, I have 300 small goals"}
)

func initMockCompany() *MockCompany {
	fakerBoss := _wireFakerBossValue
	company := &Company{
		boss: fakerBoss,
	}
	mockCompany := &MockCompany{
		company:   company,
		fakerBoss: fakerBoss,
	}
	return mockCompany
}

var (
	_wireFakerBossValue = &FakerBoss{message: "I'm faker boss, I owe 300 small goals"}
)

这里我们也是创建了两个注入器函数initCompanyinitMockCompany,它们都不需要从外部传入参数。但它们有不同的返回值,initCompany的返回值是*Company,而initMockCompany的返回值是*MockCompany。再来看看MockCompany的结构,其中包含了一个*Company和一个所需 mock 对象的实际类型。在initMockCompany函数中,这个 mock 对象会被实际注入,便达到了 mock 的效果。

现在我们总结一下 wire mock 的两种方式,方式 A方式 B的核心差异就是:方式 A中 mock 需要使用者传入,方式 A中自带 mock,使用者无需操心传入。

总结

以上就是对 wire 的详细介绍。下面来总结一下常见地需要注意的点:

  • wire 命令执行成功的前提是你的代码可以通过编译。有些时候会存在比较隐晦的问题,比如你的代码中存在循环依赖(即 package a 中依赖了 package b,同时 package b 中又依赖了 package a),这时候 IDE(如 GoLand) 是不会有错误提示的,执行 wire 也肯定会失败。有时候 wire 提示no provider found for *invalid type就可能是这个原因,此时先用go build编译代码,看错误提示是哪里出现了循环依赖,修改后再次执行go build直到编译通过,这时再执行wire。当然在排除掉代码本身的问题后,这个时候执行wire还是可能会失败,失败的原因则是使用 wire 不当导致的;
  • 如果修改了注入器函数的名称或出/入参,需要将原本调用注入器函数的代码给注释掉,否则wire执行失败,因为此时go build肯定是不通过的;
  • 当我们在注入器函数中使用panic()的写法,注意最好的是将panic()中换行写,否则wire执行可能会出现missing return这样的错误提示;
  • wire.Value()中第二个参数如果是指针类型,只能写作&XXX{},若写作new(XXX)执行wire会提示错误:argument to Value is too complex
  • 一个或多个ProviderSet中不能出现返回相同类型的提供者;

Tip:本文完整示例代码已上传至 Giteeopen in new window

后话

Go 的生态中,除了 wire 这样的静态注入工具,也有动态注入工具,如:uber 开源的 digopen in new windowinjectopen in new window。它们都是使用反射来实现的,灵活性是有了,但是性能损耗很大。 也不是说这就一定不好,任何工具都有合适它的地方,所以它们才会被创造出来,还是那句话:在合适的场景选择合适地解决方案才是应该去思考的。