Go 博客

使用 Go Cloud 的 Wire 实现编译时依赖注入

Robert van Gent
2018 年 10 月 9 日

概览

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) {...}

这项技术在小规模应用中效果很好,但大型应用程序可能有复杂的依赖关系图,导致出现一大块依赖于顺序但除此之外却并不那么有趣(interesting)的初始化代码。由于某些依赖项会被多次使用,因此很难干净地分解这段代码。将服务的某个实现替换为另一个实现可能会很痛苦,因为它涉及到修改依赖关系图,添加一整套新的依赖项(及其依赖项……),并删除未使用的旧依赖项。实际上,在具有大型依赖关系图的应用程序中,对初始化代码进行更改既乏味又缓慢。

像 Wire 这样的依赖注入工具旨在简化初始化代码的管理。您可以将服务及其依赖项描述为代码或配置,然后 Wire 会处理生成的依赖关系图,以确定顺序以及如何将每个服务所需的内容传递给它。通过更改函数签名或添加/删除初始化程序来更改应用程序的依赖项,然后让 Wire 完成为整个依赖关系图生成初始化代码的繁琐工作。

为什么这是 Go Cloud 的一部分?

Go Cloud 的目标是通过为有用的 Cloud 服务提供符合 Go 习惯的 API,使编写可移植的 Cloud 应用程序变得更容易。例如,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 的 digFacebook 的 inject 都使用反射来进行运行时依赖注入。Wire 主要受到 Java 的Dagger 2 的启发,并使用代码生成而不是反射或服务定位器

我们认为这种方法有几个优点:

  • 当依赖关系图变得复杂时,运行时依赖注入可能难以理解和调试。使用代码生成意味着运行时执行的初始化代码是常规的、符合 Go 习惯的代码,易于理解和调试。没有什么会被一个进行“魔法”操作的中间框架所混淆。特别是,诸如忘记依赖项之类的问题会变成编译时错误,而不是运行时错误。
  • 服务定位器不同,无需创建任意的名称或键来注册服务。Wire 使用 Go 类型来连接组件与其依赖项。
  • 更容易避免依赖膨胀。Wire 生成的代码将仅导入您需要的依赖项,因此您的二进制文件不会有未使用的导入。运行时依赖注入器无法在运行时识别未使用的依赖项。
  • Wire 的依赖关系图是静态可知的,这为工具和可视化提供了机会。

它是如何工作的?

Wire 包含两个基本概念:提供程序(providers)和注入器(injectors)。

提供程序是普通的 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,因此我们可以将 NewUserStoreNewDefaultConfig 分组到一个 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 很有帮助地告诉我们涉及的行号和类型。我们可以将它(ConnectionInfo)的提供程序添加到 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 公司问卷调查
博客索引