Go Cloud Wire:编译时依赖注入详解

未匹配的标注

本文为官方 Go Blog 的中文翻译,详见 翻译说明

Robert van Gent
2018 年 10 月 9 日

概述

Go 团队最近 公布 了具有可移植性 Cloud API 和工具的开源项目 Go Cloud,用于 开放云 的开发。这篇文章更详细地介绍了 Wire,Go Cloud 中使用的依赖注入工具。

Wire 解决了什么问题?

依赖注入 是通过显示地提供组件所需的所有依赖来生成灵活且松散耦合代码的一种标准技术。在 Go 中,通常需要将依赖项传递给构造函数:

// NewUserStore 返回一个使用 cfg 和 db 作为依赖项的 UserStore。
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}

这样的实现在小规模时效果很好,但是在大型应用程序中会有复杂的依赖关系图,从而导致一大堆依赖于顺序的初始化代码。通常很难将这样的代码干净地分解,尤其是有些依赖被多次使用的情况。迁移服务的实现时也会很痛苦,因为这会涉及添加一组新的依赖(及其依赖)并删除不再使用的旧的依赖。实际中,更改具有复杂依赖关系的应用程序初始化代码既繁琐又慢。

Wire 等依赖注入工具旨在简化初始化代码的管理。你可以通过代码或者配置来描述你的服务及其依赖项,Wire 会生成可以弄清顺序和每个服务所需依赖项的依赖关系图。你只需改变函数签名或者添加/删除初始化程序来更改应用程序的依赖项,让 Wire 来做为整个依赖关系图生成初始化代码的繁琐工作。

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

Go Cloud 的目标是通过为有用的云服务提供常用的 Go API 来简化编写便携式云应用程序的过程。例如,blob.Bucket 提供了支持 AWS S3 和 Google Cloud Storage (GCS) 的存储 API;使用  blob.Bucket 编写的应用程序可以自由切换云存储的具体实现而无需更改应用程序逻辑。但是,初始化代码本质上是特定于具体提供商的,每个提供商都有一组不同的依赖项。

例如,构建一个 GCS blob.Bucket 需要 gcp.HTTPClientgcp.HTTPClient 最终需要 google.Credentials,而 构建一个 S3blob.Bucket 需要  aws.Config, aws.Config 最终需要的是 AWS 凭证。因此,更新应用程序来使用不同的 blob.Bucket 时会涉及到我们上面描述的依赖关系图的繁琐更新。Wire 的驱动用例是简化切换 Go Cloud 便携式 API 的实现,但它也是依赖注入的通用工具。

这不是已经做过了吗?

已经有很多依赖注入的框架。 针对 Go 的有 Uber 的 digFacebook 的 inject,它们都用反射做运行时依赖注入。Wire 的灵感主要来自于 Java 的 Dagger 2,它使用代码生成,而不是反射和 服务定位器

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

  • 当依赖关系图变得复杂时,运行时依赖注入难以跟踪和调试。使用代码生成的方式意味着在运行时执行的初始化代码是常规的,惯用的 Go 代码,易于理解和调试。任何东西都不会被中间框架的「魔术」变得混淆不清。尤其是,像忘记依赖项这样的问题会变成编译时错误,而不是运行时错误。
  • 不像 服务定位器,它没有必要创建名字或者键来注册服务。Wire 使用 Go 类型连接组件及其依赖。
  • 更容易避免依赖过度。Wire 生成的代码只导入你所需要的依赖项,因此你的二进制文件不会有未使用的导入。而运行时依赖注入直到运行时才能识别未使用的依赖。
  • Wire 的依赖关系图是静态可知的,这就提供了第三方工具和可视化的可能。

它如何工作?

Wire 有两个基本概念:提供程序和注入器。

提供程序 是普通的 Go 函数,它提供给定依赖项的值,依赖项被简单地描述为函数的参数。下面是定义了三个提供程序的实例代码:

// NewUserStore 与我们在上面看到的函数相同;它是 UserStore 的提供程序,
// 依赖于 *Config 和 *mysql.DB。
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}

// NewDefaultConfig 是 *Config 的提供程序,它没有依赖。
func NewDefaultConfig() *Config {...}

// NewDB 是 *mysql.DB 的提供程序,它依赖于一些连接信息。
func NewDB(info *ConnectionInfo) (*mysql.DB, error) {...}

可以将经常一起使用的提供程序组成 ProviderSets 。 例如,在创建 *UserStore 时通常使用默认的 *Config,因此我们可以在一个 ProviderSet 里包含 NewUserStoreNewDefaultConfig

var UserStoreSet = wire.ProviderSet(NewUserStore, NewDefaultConfig)

注入器 是按照依赖关系顺序调用提供程序生成的函数。你需要写注入器的函数签名,将所有需要的输入作为参数,并插入一条对 wire.Build 的调用,该调用需要携带构造最终结果的提供程序列表或者提供程序集合:

func initUserStore() (*UserStore, error) {
    // 我们将会得到一个错误,因为 NewDB 需要 *ConnectionInfo,
    // 但是我们没有提供。
    wire.Build(UserStoreSet, NewDB)
    return nil, nil  // 这些返回值会被忽略。
}

现在我们运行一下 go generate 来执行连接:

$ 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  // 忽略这些返回值
}

现在 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 成为开发者构建便携式云应用程序的首选语言。

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
上一篇 下一篇
Summer
贡献者:2
讨论数量: 0
发起讨论 只看当前版本


暂无话题~