导语
“成熟的工具,要学会自己写代码”。本文介绍了 Go 依赖注入工具 [[Wire]] 及其使用方法,以及在实践中积累的各种运用技巧。当代码达到一定规模后,[[Wire]] 在组件解耦、开发效率、可维护性上都能发挥很大的作用,尤其在大仓场景。
依赖注入
当项目变得越来越大,代码中的组件也越来越多:各种数据库、中间件的客户端连接,分层设计中的各种库表 repositories 实例、services 实例……
这时为了代码的可维护性,应该避免组件之间的耦合。具体的做法可以遵守一个重要的设计准则:所有依赖应该在组件初始化时传递给它,这就是依赖注入(Dependency injection)。
Dependency injection is a standard technique for producing flexible and loosely coupled code, by explicitly providing components with all of the dependencies they need to work.
– Go 官方博客
下面是个简单的例子,所有组件 Message
、Greeter
、Event
自身的依赖都在初始化的时候获得。
|
|
Wire 介绍
当项目中实例依赖(组件)的数量越来越多,如果还是人工手动编写初始化代码和维护组件之间依赖关系的话,会是一件非常繁琐的事情,而且在大仓中尤其明显。因此,社区里已经有了不少的依赖注入框架。
除了来自 Google 的 Wire 以外,还有 Dig(Uber) 、Inject(Facebook)。其中 Dig 和 Inject 都是基于 Golang 的 Reflection 来实现的。这不仅对性能产生影响,而且依赖注入的机制对使用者不透明,非常的“黑盒”。
Clear is better than clever ,Reflection is never clear.
— Rob Pike
相比之下,Wire 完全基于代码生成。在开发阶段,wire 会自动生成组件的初始化代码,生成代码人类可读,可以提交仓库,也可以正常编译。因此 Wire 的依赖注入非常透明,也不会带来运行阶段的任何性能损耗。
上手介绍
这里快速介绍一下 Wire 的使用方法
第一步:下载安装 Wire
下载安装 wire 命令行工具
|
|
第二步:创建 wire.go 文件
在生成代码之前,我们先声明各个组件的依赖关系和初始化顺序。在应用入口创建一个 wire.go 文件。
cmd/web/wire.go
|
|
这个文件不会参与编译,只是为了告诉 Wire 各个组件的依赖关系,以及期望的生成结果。在这个文件:我们期望 Wire 生成一个返回 App
实例或 error
的 CreateApp
函数,App
实例初始化所需要的全部依赖都由 ProviderSet
这个组件列表提供,而 ProviderSet
声明了所有可能需要的组件的获取/初始化方法,也暗示组件之间的依赖顺序。
组件的获取/初始化方法,在 Wire 中叫做“组件的 provider”
还有几点需要注意:
- 文件开头必须带上
// +build wireinject
和随后的空行,否则会影响编译 - 在这个文件中,编辑器和 IDE 可能无法提供代码提示,但没关系,稍后会介绍如何解决这个问题
- 其中
CreateApp
的返回(两个 nil)没有任何意义,只是为了兼容 Go 语法。
第三步:生成初始化代码
命令行执行 wire ./...
,然后就能得到下面这个自动生成的代码文件。
cmd/web/wire_gen.go
|
|
第四步:使用初始化代码
Wire 已经帮我们生成了真正的 CreateApp
初始化方法,现在可以直接使用它。
cmd/web/main.go
|
|
使用技巧
组件按需加载
Wire 有个优雅的特点,不管在 wire.Build
中传入了多少个组件的 provider,Wire 始终只会按照实际需要来初始化组件,所有不需要的组件都不会生成相应的初始化代码。
因此,我们在使用时可以尽可能地提供更多的 provider,把挑选组件的工作交给 Wire。这样我们在开发时不管引用新组件、还是弃用老组件,都不需要修改初始化步骤的代码 wire.go。
比如,可以把 services 层中所有的实例构造器都提供出去。
pkg/services/wire.go
|
|
在初始化中,尽可能地引用所有可能需要的组件 provider。
cmd/web/wire.go
|
|
在后续开发中,如果需要引用新组件,只需要加到参数里即可。Wire 会任劳任怨地按照实际需要,生成需要的组件的初始化代码。
|
|
即使 Wire 找不到组件的 provider,也会提前在编译阶段报错,不会在线上运行阶段出现问题。
wire: cmd/api/wire.go:23:1: inject CreateApp: no provider found for *io.WriteCloser
编辑器与 IDE 的辅助配置
因为 wire.go
文件中加了这行注释,Go 在编译时会跳过这个文件,但也因此会影响编辑器和 IDE 的代码提示。当你在编辑 wire.go
文件时,常见的编辑器和 IDE 都无法正常地提供代码补全和错误提示功能。
|
|
但这个问题很容易解决。找到 IDE/编辑器的 Go 环境配置,在 Go Build Flags 中添加这个参数 -tags=wireinject
就可以了。
这个配置可以让编辑器和 IDE 正常地为 wire.go
文件提供代码补全和错误提示功能,开发体验提高不只一个数量级~
多个同类型组件的冲突问题
这个问题比较少见,但项目大了总是容易遇到。
Wire 通过 provider 的参数与返回类型,来判断组件的依赖关系。有时候,依赖网络中可能出现同类型的不同组件,这时 Wire 无法正确判断依赖关系,会直接报错。
provider has multiple parameters of type ...
比如下面这个 provider,依赖的 MySQL 和 PostgreSQL 客户端实例的类型是完全相同的(都是 *gorm.DB
),这时 Wire 无法根据类型正确地判断依赖关系,生成代码时会直接报错。
|
|
解决的方法也比较简单,只需要做一层类型的包装。
|
|
然后用 ProviderSerivce
代替 NewService
即可。
|
|
自动生成构造函数
当项目中充当抽象类的结构体越来越多,手动编写和维护结构体的构造函数,也是一件非常繁琐的事情。如果结构体中新增了一个指针类型的成员、却忘记更新构造函数,甚至还会引起线上 panic。
|
|
像这种繁琐、重复、容易出错的工作,就应该交给自动工具来完成。这里我毛遂自荐一个自动工具 newc(意为 “New Construtor”),它可以自动生成与更新结构体的构造函数代码。
使用方法非常简单,只需要给结构体添加这行注释。
//go:generate go run github.com/Bin-Huang/[email protected]
比如这样:
|
|
然后命令行执行 go generate ./...
即可获得构造函数代码:
constructor_gen.go
|
|
这个工具和 Wire 搭配使用,开发体验非常好。要使用新组件时,直接在结构体中添加成员就好了,不需要手动更新构造函数,也不需要考虑初始化的问题,所有重复的工作都交给自动工具(Wire 和 Newc)来完成。线下推荐过的同学,用过都说好。
当然这个工具也一定有考虑不周的情况,很期待大家的反馈和建议。
Don’t repeat yourself “DRY”
总结
Wire 可以完美地解决依赖注入的问题,但它不是一个框架,它没有”魔法“,也不是黑盒。它只是一个命令行工具,它根据实际需要,自动生成了各个组件的初始化代码。然后问题就解决了,没有额外的复杂性,没有运行的性能损耗。
Wire 和 [[Golang]] 的气质如出一辙,简单、直接、实用主义,不愧是 Go 最优雅的依赖注入工具!
Keep it simple stupid “K.I.S.S”