Golang 依赖注入工具 Wire 指南

Wire 是由 Google 开源的 Go 语言依赖注入工具,通过代码生成的形式实现依赖的自动组装。大型项目的依赖繁杂,导致初始化流程长,代码可读性和维护性较差。同时,手动维护大量依赖的初始化代码,枯燥又耗时。Wire 可以根据组件的类型,分析出依赖顺序,自动生成初始化代码,简化开发工作。

快速上手

先看个具体的例子,该例子可以分成 3 层,按照依赖关系由低到高来看,分别是 Redis、Service 和 Handler。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import (
"github.com/alicebob/miniredis"
"github.com/go-redis/redis/v8"
)

type RedisClient struct {
cli *redis.Client
}

func NewRedisClient() *RedisClient {
s, _ := miniredis.Run()
addr := s.Addr()
cli := redis.NewClient(&redis.Options{
Addr: addr,
})
return &RedisClient{cli: cli}
}

type UserService struct {
database *RedisClient
}

func NewUserService(database *RedisClient) *UserService {
return &UserService{
database: database,
}
}

type Handler struct {
userService *UserService
}

func NewHandler(userSrv *UserService) *Handler {
return &Handler{
userService: userSrv,
}
}

假如我们不使用 Wire 工具,直接手动写初始化代码。逐步调用每个的构建函数,最后组装得到 Handler 实例。依赖简单的情况下,手写也能轻松搞定,也推荐直接手写。但是如果依赖数量多,就很麻烦了,更推荐使用工具来完成。

1
2
3
redisClient := NewRedisClient()
userSvr := NewUserService(redisClient)
NewHandler(userSvr)

现在我们使用 Wire 来自动生成初始化代码,首先按照文档安装命令行工具。

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

然后在 main 文件同目录下,新建 wire.go 文件,文件内容如下。注意加上编译条件 wireinject,防止后续生成的 InitHandler 函数与当前函数同名。

返回值类型必须是最终构建对象类型,实际返回的值可以不用关心,只需要关心返回参数的类型。

函数内需要调用 wire.Build 函数,而且需要传入所有依赖的构建函数。

1
2
3
4
5
6
7
8
9
10
//+build wireinject

package main

import "github.com/google/wire"

func InitHandler() *Handler {
wire.Build(NewRedisClient, NewUserService, NewHandler)
return &Handler{}
}

接着在目录下,执行 wire 命令,预期会生成 wire_gen.go 文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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 InitHandler() *Handler {
redisClient := NewRedisClient()
userService := NewUserService(redisClient)
handler := NewHandler(userService)
return handler
}

对比上面手动写的初始化代码,完全一样,而且生成的代码没有引入额外依赖。生成的 InitHandler 函数实现了依赖注入关系,我们直接调用该函数,就可以避免自己手动实现绑定关系。

概念理解

在 Wire 中,有两个主要的概念,分别是 providerinjector

provider(构造器)是组件的构建方法,可以生成组件。provider 的输入是组件所需的依赖,返回值是相应的组件。上述例子中 NewRedisClient、NewUserService 和 NewHandler 都是 provider。

injector(注入器)是根据依赖对 provider 打包的函数,它可以构建出最终的目标对象。injector 函数的返回值类型决定最终目标对象的类型,injector 函数内部必须调用 wire.Build 函数,并将所有依赖的 provider 作为输入参数,可以不需要考虑输入参数的顺序。上述例子中,wire.go 文件中的 InitHandler 函数是 injector。

常见用法

在上面小节中,已经通过一个实际的例子讲解了 Wire 的基本用法和基础概念,本小节主要讲一些更贴近实战的用法。

异常处理

实际编程中,provider 初始化组件时可能出现异常,所以我们需要通过返回 Error 判断 injector 是否成功构建最终的目标对象。我们修改上述的例子,如果初始化 miniredis 实例失败则返回错误。

1
2
3
4
5
6
7
8
9
10
11
func NewRedisClient() (*RedisClient, error) {
s, err := miniredis.Run()
if err != nil {
return nil, err
}
addr := s.Addr()
cli := redis.NewClient(&redis.Options{
Addr: addr,
})
return &RedisClient{cli: cli}, nil
}

相应的也需要修改 injector 函数的结构,让它也支持返回错误。

1
2
3
4
func InitHandler() (*Handler, error) {
wire.Build(NewRedisClient, NewUserService, NewHandler)
return &Handler{}, nil
}

重新生成代码后,可以看到 InitHandler 函数已经支持了异常处理的能力了。

1
2
3
4
5
6
7
8
9
func InitHandler() (*Handler, error) {
redisClient, err := NewRedisClient()
if err != nil {
return nil, err
}
userService := NewUserService(redisClient)
handler := NewHandler(userService)
return handler, nil
}

输入参数

Wire 不仅可以自定义 provider 参数,也可以定义 injector 参数。比如我们需要通过配置文件修改连接 Redis 的参数,可以定义 Config 结构体,并作为 injector 的输入参数。

1
2
3
type Config struct {
PoolSize int
}

修改 NewRedisClient 函数的输入参数为 Config 类型。

1
2
3
4
5
6
7
8
9
10
11
12
func NewRedisClient(conf Config) (*RedisClient, error) {
s, err := miniredis.Run()
if err != nil {
return nil, err
}
addr := s.Addr()
cli := redis.NewClient(&redis.Options{
Addr: addr,
PoolSize: conf.PoolSize,
})
return &RedisClient{cli: cli}, nil
}

同时也需要修改 InitHandler 函数的输入参数类型。

1
2
3
4
func InitHandler(conf Config) (*Handler, error) {
wire.Build(NewRedisClient, NewUserService, NewHandler)
return &Handler{}, nil
}

重新生成后,InitHandler 函数就具备自定义输入参数的能力。

1
2
3
4
5
6
7
8
9
func InitHandler(conf Config) (*Handler, error) {
redisClient, err := NewRedisClient(conf)
if err != nil {
return nil, err
}
userService := NewUserService(redisClient)
handler := NewHandler(userService)
return handler, nil
}

高级特性

至此,已经介绍完了 Wire 的常用操作,下面主要介绍一些进阶操作。

接口绑定

Wire 是通过类型来解析依赖关系的,如果 provider 的输入参数类型为接口时,那么要求前一个 provider 的返回值也必须要是相同类型的接口。但是返回接口的写法是不太地道,Go 更推荐返回具体的类型。所以可以让前一个 provider 返回具体类型,然后手动将具体的类型绑定到接口。

Accept interfaces return concrete types

我们对 RedisClient 增加 RedisItf 接口,NewRedisClient 函数保持不变,仍然返回 RedisClient 类型。NewUserService 使用接口 RedisItf 作为输入参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
type RedisItf interface {
}

type RedisClient struct {
cli *redis.Client
}

func NewRedisClient(conf Config) (*RedisClient, error) {
s, err := miniredis.Run()
if err != nil {
return nil, err
}
addr := s.Addr()
cli := redis.NewClient(&redis.Options{
Addr: addr,
PoolSize: conf.PoolSize,
})
return &RedisClient{cli: cli}, nil
}

type UserService struct {
database RedisItf
}

func NewUserService(database RedisItf) *UserService {
return &UserService{
database: database,
}
}

修改 InitHandler 的函数体,显式地将 RedisItf 与 RedisClient 绑定。

1
2
3
4
func InitHandler(conf Config) (*Handler, error) {
wire.Build(NewRedisClient, NewUserService, NewHandler, wire.Bind(new(RedisItf), new(*RedisClient)))
return &Handler{}, nil
}

从生成的代码可以看出,NewUserService 实际的参数是 redisClient,也就是 RedisItf 的具体实现。

1
2
3
4
5
6
7
8
9
func InitHandler(conf Config) (*Handler, error) {
redisClient, err := NewRedisClient(conf)
if err != nil {
return nil, err
}
userService := NewUserService(redisClient)
handler := NewHandler(userService)
return handler, nil
}

属性注入

我们发现有些 provider 的工作非常简单,只是创建对象实例,然后给属性赋值,没有其他初始化逻辑,比如上述例子中的 NewHandler 函数。为了减少写这种重复、低级的 provider 代码,Wire 提供属性自动注入功能。
通过 wire.Struct 函数就可以指定结构体的哪些属性需要绑定。比如这里 Handler 的 userService 需要进行属性绑定。

1
2
3
4
5
6
7
8
9
func InitHandler(conf Config) (*Handler, error) {
wire.Build(
NewRedisClient,
NewUserService,
wire.Bind(new(RedisItf), new(*RedisClient)),
wire.Struct(new(Handler), "userService"),
)
return &Handler{}, nil
}

如果需要绑定 Handler 的所有属性,可以使用 * 匹配所有属性。如果想过滤特定属性,可以给 Handler 的属性加上 wire:"-" 的 tag。

1
wire.Struct(new(Handler), "*")

重新生成的代码可以完全替代 NewHandler 函数。

1
2
3
4
5
6
7
8
9
10
11
func InitHandler(conf Config) (*Handler, error) {
redisClient, err := NewRedisClient(conf)
if err != nil {
return nil, err
}
userService := NewUserService(redisClient)
handler := &Handler{
userService: userService,
}
return handler, nil
}

清理函数

如果 provider 创建的对象需要进行关闭或者回收,比如关闭文件描述符,那么就需要使用清理函数。Wire 可以支持返回闭包函数,遇到 Error 时,执行前一个 provider 返回的清理函数。我们这里将 NewRedisClient 的第二个返回参数改成闭包函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func NewRedisClient(conf Config) (*RedisClient, func(), error) {
s, err := miniredis.Run()
if err != nil {
return nil, nil, err
}
addr := s.Addr()
cli := redis.NewClient(&redis.Options{
Addr: addr,
PoolSize: conf.PoolSize,
})
cleanup := func() {
s.Close()
}
return &RedisClient{cli: cli}, cleanup, nil
}

同时将 NewUserService 支持返回错误。

1
2
3
4
5
6
7
8
func NewUserService(database RedisItf) (*UserService, error) {
if database == nil {
return nil, errors.New("nil database")
}
return &UserService{
database: database,
}, nil
}

最后将 injector 的返回类型也支持清理函数。

1
2
3
4
5
6
7
8
9
func InitHandler(conf Config) (*Handler, func(), error) {
wire.Build(
NewRedisClient,
NewUserService,
wire.Bind(new(RedisItf), new(*RedisClient)),
wire.Struct(new(Handler), "*"),
)
return &Handler{}, nil, nil
}

可以看到现在生成的代码中,如果 NewUserService 失败时,会运行 NewRedisClient 返回的清理函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func InitHandler(conf Config) (*Handler, func(), error) {
redisClient, cleanup, err := NewRedisClient(conf)
if err != nil {
return nil, nil, err
}
userService, err := NewUserService(redisClient)
if err != nil {
cleanup()
return nil, nil, err
}
handler := &Handler{
userService: userService,
}
return handler, func() {
cleanup()
}, nil
}

类型重复

如果依赖链路上,出现两个相同类型的依赖,那么 Wire 将会报错,因为它已经无法判断正确的依赖关系。解决办法就是创建新的类型,然后将返回值强转成新的类型。
我们以 Handler 举例,现在 Handler 有两个 UserService 类型的属性,按照现在的定义,代码生成会报错失败。

1
2
3
4
type Handler struct {
userService *UserService
userServiceV2 *UserService
}

解决办法很简单,就是定义 UserServiceV2,并且增加对应的 provider。同时将 Handler 的第二个属性类型改成 UserServiceV2。

1
2
3
4
5
6
7
8
9
10
11
type UserServiceV2 UserService

func NewUserServiceV2(database RedisItf) (*UserServiceV2, error) {
us := &UserService{database: database}
return (*UserServiceV2)(us), nil
}

type Handler struct {
userService *UserService
userServiceV2 *UserServiceV2
}

别忘了把新的 provider 添加到 injector 中。

1
2
3
4
5
6
7
8
9
10
func InitHandler(conf Config) (*Handler, func(), error) {
wire.Build(
NewRedisClient,
NewUserService,
NewUserServiceV2,
wire.Bind(new(RedisItf), new(*RedisClient)),
wire.Struct(new(Handler), "*"),
)
return &Handler{}, nil, nil
}

现在 Wire 就可以正确识别 Handler 的所有属性了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func InitHandler(conf Config) (*Handler, func(), error) {
redisClient, cleanup, err := NewRedisClient(conf)
if err != nil {
return nil, nil, err
}
userService, err := NewUserService(redisClient)
if err != nil {
cleanup()
return nil, nil, err
}
userServiceV2, err := NewUserServiceV2(redisClient)
if err != nil {
cleanup()
return nil, nil, err
}
handler := &Handler{
userService: userService,
userServiceV2: userServiceV2,
}
return handler, func() {
cleanup()
}, nil
}

全文总结

Wire 仅仅根据对象的类型来分析依赖关系和顺序,所以必须保证类型唯一、不重复,只有这样才不会产生歧义的构造关系。

Wire 主要由 provider 和 injector 两部分组成,按照返回值类型来区分 provider 的话,provider 只有 3 种写法。以 NewRedisClient 为例,3 种写法如下所示。

1
2
3
NewRedisClient() *RedisClient
NewRedisClient() (*RedisClient, error)
NewRedisClient() (*RedisClient, func(), error)

本文列举的用法都是 Wire 实际编程中常用的操作,掌握之后可以解决大部分使用过程中的问题。更详细的使用建议参考官方文档。