[译]使用Go Cloud的Wire进行编译时依赖注入

2018年10月9日

概述

Go团队最近公布了用于开放云开发的可移植云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提供了一个存储API,其中包含亚马逊S3和谷歌云存储(GCS)的实现; 使用blob.Bucket编写的应用程序可以交换实现而无需更改其应用程序逻辑。 但是,初始化代码本质上是特定于提供者的,并且每个提供者具有不同的依赖集。

例如, 构建GCS blob.Bucket需要gcp.HTTPClient ,最终需要google.Credentials ,而为S3构建一个则需要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可以分组到ProviderSets 。 例如,在创建*UserStore时使用默认的*Config是很常见的,因此我们可以在ProviderSet中对NewUserStoreNewDefaultConfig进行分组:

var UserStoreSet = wire.ProviderSet(NewUserStore, NewDefaultConfig)

注入器是被生成的函数,它们按依赖所需的顺序调用提供者。 编写注入器的签名,包括任何所需的输入作为参数,并插入对wire.Build的调用, wire.Build包含构造最终结果所需的提供者或提供者集的列表:

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

现在我们运行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  // 这些返回值会被忽略。
}

现在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使用体验的任何意见! Go Cloud的开发是在GitHub上进行的,所以你可以提出一个问题来告诉我们什么可能更好。 有关项目的更新和讨论,请加入项目的邮件列表

感谢您抽出宝贵时间了解Go Cloud的Wire。 我们很高兴与您合作,使Go成为构建可移植云应用程序的开发人员的首选语言。

作者:Robert van Gent

原文:https://blog.golang.org/wire

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,596评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,451评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,691评论 2 59
  • 在职场中,每家公司都需要招聘人员,人员的更迭,优胜汰弱变成了企业发展中最重要的一环。 在面试的过程中,面试官都喜欢...
    萍行职场阅读 1,952评论 0 0
  • 一千个人眼里有一千种爱情,每一种都是最美的模样,所以别去评论别人的幸福。没经历过的都没资格,就像人生一样,好的坏的...
    码字的黄小邪阅读 1,380评论 17 16