Go 博客
使用 Go Cloud 的 Wire 进行编译时依赖注入
概述
Go 团队最近宣布了开源项目Go Cloud,该项目提供了可移植的 Cloud API 和工具,用于开放云开发。本文将更详细地介绍 Wire,这是一个在 Go Cloud 中使用的依赖注入工具。
Wire 解决了什么问题?
依赖注入是一种标准技术,通过显式地向组件提供其工作所需的所有依赖项来生成灵活且松散耦合的代码。在 Go 中,这通常采用将依赖项传递给构造函数的形式。
// NewUserStore returns a UserStore that uses cfg and db as dependencies.
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}
这种技术在小规模应用中效果很好,但大型应用可能具有复杂的依赖关系图,从而导致一个依赖于顺序、但本身不那么有趣的巨大初始化代码块。通常很难干净地分解这段代码,特别是因为某些依赖项会被多次使用。用另一种实现替换服务的实现可能会很痛苦,因为它需要修改依赖关系图,添加一整套新的依赖项(及其依赖项…),并删除未使用的旧依赖项。实际上,在具有大型依赖关系图的应用中修改初始化代码既繁琐又缓慢。
Wire 等依赖注入工具旨在简化初始化代码的管理。您将服务及其依赖项描述为代码或配置,然后 Wire 处理生成的图以确定顺序以及如何向每个服务传递所需的内容。通过更改函数签名或添加或删除初始化程序来更改应用的依赖项,然后让 Wire 完成生成整个依赖关系图初始化代码的繁琐工作。
为什么这是 Go Cloud 的一部分?
Go Cloud 的目标是通过为有用的云服务提供惯用的 Go API,使编写可移植的云应用变得更容易。例如,blob.Bucket 提供了一个存储 API,它具有 Amazon S3 和 Google Cloud Storage (GCS) 的实现;使用 blob.Bucket
编写的应用可以在不更改其应用逻辑的情况下切换实现。然而,初始化代码本质上是提供商特定的,并且每个提供商都有不同的依赖项集合。
例如,构造一个 GCS blob.Bucket
需要一个 gcp.HTTPClient
,它最终需要 google.Credentials
,而构造一个用于 S3 的 blob.Bucket
需要一个 aws.Config
,它最终需要 AWS 凭证。因此,将应用更新为使用不同的 blob.Bucket
实现,会涉及到我们上面描述的那种对依赖关系图进行繁琐的更新。Wire 的主要用例是使 Go Cloud 可移植 API 的实现切换变得容易,但它也是一个通用的依赖注入工具。
这不是已经有人做过了吗?
市面上有许多依赖注入框架。对于 Go 而言,Uber 的 dig 和 Facebook 的 inject 都使用反射进行运行时依赖注入。Wire 主要受 Java 的 Dagger 2 启发,并使用代码生成而非反射或服务定位器。
我们认为这种方法有几个优点
- 当依赖关系图变得复杂时,运行时依赖注入可能难以跟踪和调试。使用代码生成意味着运行时执行的初始化代码是常规的、惯用的 Go 代码,易于理解和调试。没有被介入的框架进行“魔法”所混淆。特别是,忘记依赖项之类的问题会变成编译时错误,而不是运行时错误。
- 与服务定位器不同,无需编造任意名称或键来注册服务。Wire 使用 Go 类型来连接组件及其依赖项。
- 更容易避免依赖膨胀。Wire 生成的代码只会导入您需要的依赖项,因此您的二进制文件不会包含未使用的导入。运行时依赖注入器直到运行时才能识别未使用的依赖项。
- Wire 的依赖关系图是静态可知的,这为工具和可视化提供了机会。
它是如何工作的?
Wire 有两个基本概念:提供者(providers)和注入器(injectors)。
提供者(Providers)是普通的 Go 函数,它们根据其依赖项“提供”值,依赖项简单地描述为函数的参数。这里有一些定义三个提供者的示例代码:
// 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) {...}
常用的一组提供者可以归入 ProviderSets
。例如,在创建 *UserStore
时通常使用默认的 *Config
,因此我们可以将 NewUserStore
和 NewDefaultConfig
组合到 ProviderSet
中。
var UserStoreSet = wire.ProviderSet(NewUserStore, NewDefaultConfig)
注入器(Injectors)是生成的函数,它们按依赖顺序调用提供者。您编写注入器的签名,包括作为参数的任何所需输入,并插入对 wire.Build
的调用,其中包含构造最终结果所需的提供者或提供者集合列表。
func initUserStore() (*UserStore, error) {
// We're going to get an error, because NewDB requires a *ConnectionInfo
// and we didn't provide one.
wire.Build(UserStoreSet, NewDB)
return nil, nil // These return values are ignored.
}
现在我们运行 go generate 来执行 wire。
$ go generate
wire.go:2:10: inject initUserStore: no provider found for ConnectionInfo (required by provider of *mysql.DB)
wire: generate failed
噢!我们没有包含 ConnectionInfo
,也没有告诉 Wire 如何构建它。Wire 会很友好地告诉我们相关的行号和类型。我们可以为其添加一个提供者到 wire.Build
中,或者将其作为参数添加。
func initUserStore(info ConnectionInfo) (*UserStore, error) {
wire.Build(UserStoreSet, NewDB)
return nil, nil // These return values are ignored.
}
现在 go generate
将创建一个包含生成代码的新文件。
// File: wire_gen.go
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
func initUserStore(info ConnectionInfo) (*UserStore, error) {
defaultConfig := NewDefaultConfig()
db, err := NewDB(info)
if err != nil {
return nil, err
}
userStore, err := NewUserStore(defaultConfig, db)
if err != nil {
return nil, err
}
return userStore, nil
}
所有非注入器声明都会复制到生成的文件中。运行时没有对 Wire 的依赖:所有编写的代码都只是普通的 Go 代码。
如您所见,输出与开发者自己编写的代码非常接近。这是一个只有三个组件的简单示例,因此手动编写初始化程序不会太痛苦,但对于具有更复杂依赖关系图的组件和应用来说,Wire 节省了大量手动工作。
我如何参与并了解更多?
Wire README 更详细地介绍了如何使用 Wire 及其更高级的功能。还有一个教程,通过一个简单的应用演示了 Wire 的使用。
我们感谢您对 Wire 的任何使用体验反馈!Wire 的开发在 GitHub 上进行,因此您可以提交一个 Issue 来告诉我们哪些地方可以改进。要获取项目更新和讨论,请加入Go Cloud 邮件列表。
感谢您花时间了解 Go Cloud 的 Wire。我们很高兴与您一起努力,使 Go 成为开发可移植云应用的开发者的首选语言。
下一篇文章:宣布 App Engine 的新 Go 1.11 运行时
上一篇文章:参与 2018 年 Go 公司调查问卷
博客索引