【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. |