【Golang】Dependency Injection
依赖注入是一种通用技术,通过显式地为组件提供它们工作所需的所有依赖关系,生成灵活且松耦合的代码。在Go语言中,我们经常采用下面这样的方式为构造器传递依赖:
1 | // NewUserStore returns a UserStore that uses cfg and db as dependencies. |
这种技术在小规模上效果很好,但较大的应用程序可能有一个复杂的依赖关系图,导致一大块初始化代码依赖于顺序。通常很难干净地分解这段代码,尤其是某些依赖项被多次使用。如果涉及到服务替换可能会更痛苦,因为它涉及通过添加一组全新的依赖项,我们需要修改依赖关系图。如果大家干过这种事情,发现这种代码的修改很繁琐。
依赖注入工具旨在简化初始化代码的管理,我们只需要将自己的服务及其依赖关系描述为代码或配置,然后依赖注入工具会处理生成的依赖关系图,确定排序并且为每个服务自动传递所需要的依赖。通过更改函数签名,添加或删除初始化程序就可以更改应用程序的依赖项,然后依赖注入完成为整个依赖关系图生成初始化代码的繁琐工作。
在Go语言中,这样依赖工具有不少,例如:dig,inject 以及 wire。这次我们着重介绍 wire
,相对其他两个有如下优势:
-
wire
使用代码生成而不是运行时反射。因为当依赖图变得复杂时,运行时依赖注入可能很难跟踪和调试。使用代码生成意味着在运行时执行的初始化代码是常规的、惯用的 Go 代码,易于理解和调试; -
wire
使用Go类型名称识别依赖项,不用像其他的服务定位器,需要为每个依赖项定义一个名称; -
wire
更容易避免依赖膨胀。Wire
生成的代码只会导入需要的依赖项,因此二进制文件不会有未使用的导入。然而运行时依赖注入器直到运行时才能识别未使用的依赖项; -
Wire
的依赖图是静态可知的,便于工具可视化;
工作原理
Wire
有两个基本的概念,providers
和 injectors
,provider
其实就是Go原生的函数,它们接受某些参数,称之为返回值的依赖,然后返回某个想要的类型示例。例如:
1 | // NewUserStore is the same function we saw above; it is a provider for UserStore, |
可以对经常一起使用的 provider
进行分组,例如创建上面的 UserStore
实例的时候都会使用 *Config
,所以,我们可以将 NewUserStore
和 NewDefaultConfig
分组成一个 ProviderSet
。
1 | var UserStoreSet = wire.NewSet(NewUserStore, NewDefaultConfig) |
Injectors
是按依赖顺序调用 providers
的函数。我们可以按照下面这样的格式写 injector
的签名,包括任何需要的参数,特殊的是需要插入一个带有一系列 provides
或者 providerSet
的 wire.Build
:
1 | func initUserStore(info *ConnectionInfo) (*UserStore, error) { |
最佳实践
使用标准的 go install
即可安装 wire
工具:
go install github.com/google/wire/cmd/wire@latest
使用 go get
命令安装 wire
扩展:
使用我们前一小节介绍时使用到的代码来完成最后的代码生成,我们的代码目录结构和其中代码应该是这个样子的:
1 | ~/WORKDIR/gostudy/di ⌚ 22:07:19 |
1 | package di |
这里必须添加一个条件编译指令,wire
命令会识别它,在 go1.17
及其之后是:
//go:build wireinject
go1.17
之前是:
// +build wireinject
详细的变动可以看 Go1.17 release notes 和 https://go.dev/design/draft-gobuild。
1 | //go:build wireinject |
initUserStore
是我们的业务代码实际使用到的初始化函数,里面的依赖逻辑和顺序由 wire
帮我们自动维护。
1 | // Code generated by Wire. DO NOT EDIT. |