dig 是 uber 开源的一个依赖注入组件,内部基于反射实现,适用于:
- 为程序框架(如 Fx )提供依赖注入能力;
- 在进程启动阶段,解决对象依赖图;
但不适用于以下场景:
- 它只提供依赖注入功能,无法替代程序框架(如 Fx );
- 在进程启动阶段后的依赖解决;
- 作为服务发现组件,暴露给其他用户代码;
安装
开始使用 dig 解决依赖问题前,我们需要先安装,推荐安装主版本 1 :
|
|
Container
dig 提供了一容器( container ),来解决有向无环图式依赖。调用 New 函数即可创建一个容器:
|
|
程序组件间的依赖关系是一个有向图,边指向被依赖组件。通常程序不允许循环依赖,这意味着图不能有环,因此是一个有向无环图。
Provide
dig 容器负责管理依赖关系,并接管了对象创建工作。因此,我们必须告诉 container 每个数据类型分别应该如何创建。这通过 Container 提供的 Provide 方法来进行。
Provide 方法接收一个构造器函数作为参数,该函数负责具体数据类型的创建和初始化,并将其返回。如果待创建的数据类型依赖其他类型,可以将依赖作为构造函数参数来声明。
所有依赖的构造器函数也必须注册进 Container ,但顺序没有严格要求,在当前类型前后均可:
|
|
这段代码注册了用户网关 UserGateway 的构造函数,告诉 dig 容器 UserGateway 依赖数据库连接 *sql.DB ,以及详细的创建过程。dig 接到这个构造器后,通过 reflect 反射检查构造器参数返回值,即可获悉这些信息。
最终在创建 UserGateway 时,dig 先取得 *sql.DB ,并以它为参数执行构造函数。注意到,代码在第 8 行处注册了数据库连接的构造函数,由此 dig 知道如何创建 *sql.DB 。
一个类型可以被多个构造器依赖,但不会被重复创建。因为 dig 容器会为每个注册的类型,维护一个单例( singleton )。单例在第一次用到时创建,而且最多只创建一次。
|
|
构造函数可以声明任意个依赖,如有必要也可返回 error :
|
|
这段代码注册了 *RequestHandler 的构造函数,它有两个依赖,分别是 UserGateway 和 CommentGateway 。
构造函数也可以返回多个值,同时完成多个类型的初始化:
|
|
这段代码注册的构造函数,依赖数据库连接 *sql.DB ,负责 UserGateway 和 CommentGateway 的创建工作。
构造函数可以接收可变参数,但 dig 执行时不会为其传参:
|
|
因此,对 dig 来说,这个构造函数等价于:
|
|
Invoke
类型构造函数加入 dig 容器后,可以通过 Invoke 方法来发起构建请求。跟 Provide 一样,Invoke 方法也接收一个函数作为参数。函数通过参数指定要请求的数据类型,dig 负责创建这些类型,并执行该函数:
|
|
这段代码调用 Invoke 方法请求日志对象 *log.Logger , dig 先完成 *log.Logger 的创建工作,并将其传给该函数。匿名函数里可以执行任何需要用到 *log.Logger 的处理逻辑,或者将其记下后再其他地方使用。
请求函数如有必要,也可以返回 error ,error 则被 Invoke 传给调用者:
|
|
如果请求的类型,或者依赖类型在容器中不可用,Invoke 方法也将报错。
参数对象
构造函数通过参数声明依赖,依赖一多很快就会影响可读性:
|
|
有个编程模式可以提高这种场景的可读性:创建一个结构体,将函数参数作为结构体字段,修改原函数以该结构体为参数。这个结构体就是所谓的 参数对象( parameter object )。
dig 对参数对象支持到位:任何结构体只要嵌入 dig.In 即视为参数对象。上一构造器可以这样改写:
|
|
构造函数支持同时接受参数对象和普通参数,可以任意组合:
|
|
结果对象
跟参数对象相对的是 结果对象( result object ),用来描述构造函数返回的多个值。结果对象也是一个结构体,每个字段表示构造函数创建的一个值。任何结构体只要嵌入 dig.Out ,即视为结果对象。
|
|
这个构造器负责创建 *UserGateway 、*CommentGateway 和 *PostGateway ,可以使用结果对象改写成这样:
|
|
可选依赖
在某些场景下,就算依赖缺失,组件也可降级工作。因此,组件构造函数不用强制依赖其他类型。dig 支持声明可选依赖,只要在参数对象相关字段上打上 optional:"true"
标签即可:
|
|
如果可选依赖字段在容器中不存在,构造函数会收到该字段的零值:
|
|
构造函数声明可选依赖后,必须处理依赖缺失的情况。有了可选依赖后,我们还能在不破坏当前代码结构的前提下,添加新的依赖项。
命名依赖
某些使用场景可能会为同个数据类型初始化多个实例,例如数据库连接可分为只读和读写:
|
|
dig 支持对实例值进行命名,将同个类型的多个不同实例加入容器。注册构造函数时可以传 dig.Name 参数,告诉 dig 对构造函数返回的实例值进行命名。如果构造函数返回多个值,每个值都会被命名。
我们将上述两个数据库连接构造函数注册到 dig 容器,并通过可选参数 dig.Name 将它们分别命名为 ro 和 rw :
|
|
另一种方式,在结果对象中为字段打上命名标签:name:"xxxx"
,从而为实例值命名:
|
|
无论依赖用哪种方式命名,其他构建函数都可以通过参数对象准确获取。参数对象字段必须打上命名标签,指定要获取的依赖实例名,dig 据此注入同名且同类型的实例对象:
|
|
命名标签和可选依赖标签可以组合使用,声明可选的命名依赖:
|
|
CommentGateway 的依赖由参数对象 GatewayParams 声明,它依赖两个数据库连接对象,一个是可读可写,另一个是只读的。注意到,只读连接是可选的,只读连接缺失则直接使用读写连接。
实例组
为实现同一类型的多实例构建或消费,dig 引入了实例组( value group )的概念。实例组是容器内部的一个乱序命名集合,构造函数可以往该集合添加实例值,而其他构造函数可以请求集合中的所有实例,结果以 切片( slice )的形式返回。
构造函数只要返回打上 group:"xxxx"
标签的 dig.Out 结果对象,相关字段就会被添加到实例组:
|
|
这段代码中两个构造函数注册后,dig 会将它们创建的 Handler 实例值添加到名为 server 的实例组。可能有任意多的构造函数往该命名组添加实例,而且其他构造函数借助同样打上 group:"xxxx"
标签的切片字段,即可请求全部实例值:
|
|
*Server 构造函数参数对象通过 Handler 字段请求 server 组中的所有 Handler 实例。dig 会先执行所有向该实例组添加实例值的构造函数,但执行顺序没有明确规定。
实例组中的实例值是无序的,因为这些实例值的构建函数以什么顺序执行,dig 不作任何保证。
在某些场景,我们可能需要在 dig.Out 中添加切片字段,以便同时向实例值添加多个值。然而,考虑到实例组必须通过切片来请求的原则,我们只能通过切片的切片来获取这些实例值。
好在 v1.9.0 版本后,dig 支持将切片中的元素逐个加入实例值,而不是将切片本身。想要实现这种效果,只需要为 group 标签打上 flatten 修饰语即可:
|
|
- 第 4 行将切片本身添加进实例组,必须通过切片的切片(
[][]int
)来获取; - 第 5 行将切片中的元素逐一加入实例组,通过实例切片(
[]int
)获取即可;
【小菜学Go语言】系列文章首发于公众号【小菜学编程】,敬请关注: