【Golang】Dependency Injection

依赖注入是一种通用技术,通过显式地为组件提供它们工作所需的所有依赖关系,生成灵活且松耦合的代码。在Go语言中,我们经常采用下面这样的方式为构造器传递依赖:

1
2
// NewUserStore returns a UserStore that uses cfg and db as dependencies.
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}

这种技术在小规模上效果很好,但较大的应用程序可能有一个复杂的依赖关系图,导致一大块初始化代码依赖于顺序。通常很难干净地分解这段代码,尤其是某些依赖项被多次使用。如果涉及到服务替换可能会更痛苦,因为它涉及通过添加一组全新的依赖项,我们需要修改依赖关系图。如果大家干过这种事情,发现这种代码的修改很繁琐。

依赖注入工具旨在简化初始化代码的管理,我们只需要将自己的服务及其依赖关系描述为代码或配置,然后依赖注入工具会处理生成的依赖关系图,确定排序并且为每个服务自动传递所需要的依赖。通过更改函数签名,添加或删除初始化程序就可以更改应用程序的依赖项,然后依赖注入完成为整个依赖关系图生成初始化代码的繁琐工作。

在Go语言中,这样依赖工具有不少,例如:diginject 以及 wire。这次我们着重介绍 wire,相对其他两个有如下优势:

  1. wire 使用代码生成而不是运行时反射。因为当依赖图变得复杂时,运行时依赖注入可能很难跟踪和调试。使用代码生成意味着在运行时执行的初始化代码是常规的、惯用的 Go 代码,易于理解和调试;

  2. wire 使用Go类型名称识别依赖项,不用像其他的服务定位器,需要为每个依赖项定义一个名称;

  3. wire 更容易避免依赖膨胀。 Wire 生成的代码只会导入需要的依赖项,因此二进制文件不会有未使用的导入。然而运行时依赖注入器直到运行时才能识别未使用的依赖项;

  4. Wire 的依赖图是静态可知的,便于工具可视化;

工作原理

Wire 有两个基本的概念,providersinjectorsprovider 其实就是Go原生的函数,它们接受某些参数,称之为返回值的依赖,然后返回某个想要的类型示例。例如:

1
2
3
4
5
6
7
8
9
// NewUserStore is the same function we saw above; it is a provider for UserStore,
// with dependencies on *Config and *mysql.DB.
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}

// NewDefaultConfig is a provider for *Config, with no dependencies.
func NewDefaultConfig() *Config {...}

// NewDB is a provider for *mysql.DB based on some connection info.
func NewDB(info *ConnectionInfo) (*mysql.DB, error) {...}

可以对经常一起使用的 provider 进行分组,例如创建上面的 UserStore 实例的时候都会使用 *Config,所以,我们可以将 NewUserStoreNewDefaultConfig 分组成一个 ProviderSet

1
var UserStoreSet = wire.NewSet(NewUserStore, NewDefaultConfig)

Injectors 是按依赖顺序调用 providers 的函数。我们可以按照下面这样的格式写 injector 的签名,包括任何需要的参数,特殊的是需要插入一个带有一系列 provides 或者 providerSetwire.Build

1
2
3
4
func initUserStore(info *ConnectionInfo) (*UserStore, error) {
wire.Build(UserStoreSet, NewDB)
return nil, nil // These return values are ignored.
}

最佳实践

使用标准的 go install 即可安装 wire 工具:

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

使用 go get 命令安装 wire 扩展:

go get github.com/google/wire@latest

使用我们前一小节介绍时使用到的代码来完成最后的代码生成,我们的代码目录结构和其中代码应该是这个样子的:

1
2
3
4
5
6
~/WORKDIR/gostudy/di ⌚ 22:07:19
$ ll
total 24
-rw-r--r-- 1 fudenglong staff 240B 3 29 22:05 injectors.go
-rw-r--r-- 1 fudenglong staff 700B 3 29 22:04 providers.go
-rw-r--r-- 1 fudenglong staff 448B 3 29 22:05 wire_gen.go // 由 wire 命令生成
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
package di

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

type Config struct{}

type DB struct{}

type UserStore struct{}

type ConnectionInfo struct{}

var ErrInvalidConnectionInfo = errors.New("invalid connection info")

func NewUserStore(cfg *Config, db *DB) (*UserStore, error) {
return &UserStore{}, nil
}

// NewDefaultConfig is a provider for *Config, with no dependencies.
func NewDefaultConfig() *Config {
return &Config{}
}

// NewDB is a provider for *mysql.DB based on some connection info.
func NewDB(info *ConnectionInfo) (*DB, error) {
if info == nil {
return nil, ErrInvalidConnectionInfo
}
return &DB{}, nil
}

var UserStoreSet = wire.NewSet(NewUserStore, NewDefaultConfig)

这里必须添加一个条件编译指令,wire 命令会识别它,在 go1.17 及其之后是:

//go:build wireinject

go1.17 之前是:

// +build wireinject

详细的变动可以看 Go1.17 release noteshttps://go.dev/design/draft-gobuild

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

package di

import "github.com/google/wire"

func initUserStore(info *ConnectionInfo) (*UserStore, error) {
wire.Build(UserStoreSet, NewDB)
return nil, nil // These return values are ignored.
}

initUserStore 是我们的业务代码实际使用到的初始化函数,里面的依赖逻辑和顺序由 wire 帮我们自动维护。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Code generated by Wire. DO NOT EDIT.

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

package di

// Injectors from injectors.go:

func initUserStore(info *ConnectionInfo) (*UserStore, error) {
config := NewDefaultConfig()
db, err := NewDB(info)
if err != nil {
return nil, err
}
userStore, err := NewUserStore(config, db)
if err != nil {
return nil, err
}
return userStore, nil
}

参考文章

  1. Compile-time Dependency Injection With Go Cloud’s Wire