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 的一部分?
blob.Bucket
blob.Bucketgcp.HTTPClientgcp.HTTPClientgoogle.Credentialsblob.Bucketaws.Configaws.Configblob.Bucket
这不是已经做过了吗?
已经有很多依赖注入的框架。 针对 Go 的有 Uber 的 dig 和 Facebook 的 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*ConfigProviderSetNewUserStoreNewDefaultConfig
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
ConnectionInfowire.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 成为开发者构建便携式云应用程序的首选语言。