Go 博客
使用 Go Cloud 的 Wire 进行编译时依赖注入
概述
Go 团队最近宣布开源项目Go 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 构造一个需要 aws.Config
,后者最终需要 AWS 凭据。因此,更新应用程序以使用不同的 blob.Bucket
实现涉及我们上面描述的关于依赖项图的乏味更新。Wire 的主要用例是简化 Go Cloud 可移植 API 实现的交换,但它也是一个用于依赖注入的通用工具。
这难道不是已经做过了吗?
有很多依赖注入框架。对于 Go,Uber 的 dig 和Facebook 的 inject 都使用反射来进行运行时依赖注入。Wire 主要受到 Java 的Dagger 2 的启发,并使用代码生成而不是反射或服务定位器。
我们认为这种方法有几个优点
- 当依赖项图变得复杂时,运行时依赖注入可能难以理解和调试。使用代码生成意味着在运行时执行的初始化代码是常规的、惯用的 Go 代码,易于理解和调试。没有任何东西会被干预框架执行“魔术”而变得模糊不清。特别是,像忘记依赖项这样的问题会变成编译时错误,而不是运行时错误。
- 与服务定位器不同,无需为注册服务创建任意名称或键。Wire 使用 Go 类型将组件与其依赖项连接起来。
- 更容易避免依赖项膨胀。Wire 生成的代码只会导入您需要的依赖项,因此您的二进制文件不会有未使用的导入。运行时依赖注入器无法在运行时识别未使用的依赖项。
- Wire 的依赖项图可以在静态时知道,这为工具和可视化提供了机会。
它是如何工作的?
Wire 有两个基本概念:提供者和注入器。
提供者是普通的 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)
注入器是生成的函数,它们按依赖项顺序调用提供者。您编写注入器的签名,包括作为参数的任何需要的输入,并在 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 上进行,因此您可以提交问题告诉我们哪里可以改进。有关项目的更新和讨论,请加入Go Cloud 邮件列表。
感谢您抽出时间了解 Go Cloud 的 Wire。我们很高兴与您合作,使 Go 成为开发人员构建可移植云应用程序的首选语言。
下一篇文章:宣布 App Engine 的新 Go 1.11 运行时
上一篇文章:参与 2018 年 Go 公司问卷调查
博客索引