Go 语言官方依赖注入工具 Wire 使用指北

网站建设4年前发布
40 0 0

接触 Golang 有一段时间了,发现 Golang 同样需要类似 Java 中 Spring 一样的依赖注入框架。如果项目规模比较小,是否有依赖注入框架问题不大,但当项目变大之后,有一个合适的依赖注入框架是十分必要的。通过调研,了解到 Golang 中常用的依赖注入工具主要有 Inject 、Dig 等。但是今天主要介绍的是 Go 团队开发的 Wire,一个编译期实现依赖注入的工具。,说起依赖注入​就要引出另一个名词控制反转​( IoC )。IoC 是一种设计思想,其核心作用是降低代码的耦合度。依赖注入​是一种实现控制反转且用于解决依赖性问题的设计模式。,举个例子,假设我们代码分层关系是 dal 层连接数据库,负责数据库的读写操作。那么我们的 dal 层的上一层 service 负责调用 dal 层处理数据,在我们目前的代码中,它可能是这样的:,在这段代码里,层级依赖关系为 service -> dal -> db,上游层级通过 Getxxx​实例化依赖。但在实际生产中,我们的依赖链比较少是垂直依赖关系,更多的是横向依赖。即我们一个方法中,可能要多次调用Getxxx的方法,这样使得我们代码极不简洁。,不仅如此,我们的依赖都是写死的,即依赖者的代码中写死了被依赖者的生成关系。当被依赖者的生成方式改变,我们也需要改变依赖者的函数,这极大的增加了修改代码量以及出错风险。,接下来我们用依赖注入的方式对代码进行改造:,如上编码情况中,我们通过将 db 实例对象注入到 dal 中,再将 dal 实例对象注入到 service 中,实现了层级间的依赖注入。解耦了部分依赖关系。,在系统简单、代码量少的情况下上面的实现方式确实没什么问题。但是项目庞大到一定程度,结构之间的关系变得非常复杂时,手动创建每个依赖,然后层层组装起来的方式就会变得异常繁琐,并且容易出错。这个时候勇士 wire 出现了!,Wire 是一个轻巧的 Golang 依赖注入工具。它由 Go Cloud 团队开发,通过自动生成代码的方式在编译期完成依赖注入。它不需要反射机制,后面会看到, Wire 生成的代码与手写无异。,wire 的安装:,上面的命令会在 $GOPATH/bin​ 中生成一个可执行程序 wire​,这就是代码生成器。可以把$GOPATH/bin​ 加入系统环境变量 $PATH​ 中,所以可直接在命令行中执行 wire 命令。,下面我们在一个例子中看看如何使用 wire。,现在我们有这样的三个类型:,三者的 init 方法:,假设 Channel 有一个 GetMsg 方法,BroadCast 有一个 Start 方法:,如果手动写代码的话,我们的写法应该是:,如果使用 wire,我们需要做的就变成如下的工作了:,注意:需要在文件头部增加构建约束://+build wireinject,我们告诉 wire​,我们所用到的各种组件的 init​ 方法(NewBroadCast​, NewChannel​, NewMessage​),那么 wire 工具会根据这些方法的函数签名(参数类型/返回值类型/函数名)自动推导依赖关系。,wire.go​ 和 wire_gen.go​ 文件头部位置都有一个 +build​,不过一个后面是 wireinject​,另一个是 !wireinject。+build​ 其实是 Go 语言的一个特性。类似 C/C++ 的条件编译,在执行 go build​ 时可传入一些选项,根据这个选项决定某些文件是否编译。wire​ 工具只会处理有wireinject​ 的文件,所以我们的 wire.go​ 文件要加上这个。生成的 wire_gen.go​ 是给我们来使用的,wire​ 不需要处理,故有 !wireinject。,Wire​ 有两个基础概念,Provider​(构造器)和 Injector(注入器),下面简单介绍一下 wire 在飞书问卷表单服务中的应用。,飞书问卷表单服务的 project​ 模块中将 handler 层、service 层和 dal 层的初始化通过参数注入的方式实现依赖反转。通过 BuildInjector 注入器来初始化所有的外部依赖。,dal 伪代码如下:,service 伪代码如下:,handler 伪代码如下:,injector.go 伪代码如下:,在 wire.go 中如下定义:,执行 wire gen ./internal/app/wire.go 生成 wire_gen.go,在 main.go 中加入初始化 injector 的方法 app.BuildInjector,注意,如果你运行时,出现了 BuildInjector​ 重定义,那么检查一下你的 //+build wireinject​ 与 package app 这两行之间是否有空行,这个空行必须要有!见https://github.com/google/wire/issues/117,NewSet​ 一般应用在初始化对象比较多的情况下,减少 Injector​ 里面的信息。当我们项目庞大到一定程度时,可以想象会出现非常多的 Providers。NewSet​ 帮我们把这些 Providers 按照业务关系进行分组,组成 ProviderSet(构造器集合),后续只需要使用这个集合即可。,上述例子的 Provider​ 都是函数,除函数外,结构体也可以充当 Provider​ 的角色。Wire 给我们提供了结构构造器(Struct Provider)。结构构造器创建某个类型的结构,然后用参数或调用其它构造器填充它的字段。,Bind​ 函数的作用是为了让接口类型的依赖参与 Wire​ 的构建。Wire​ 的构建依靠参数类型,接口类型是不支持的。Bind 函数通过将接口类型和实现类型绑定,来达到依赖注入的目的。,构造器可以提供一个清理函数(cleanup),如果后续的构造器返回失败,前面构造器返回的清理函数都会调用。初始化 Injector​ 之后可以获取到这个清理函数,清理函数典型的应用场景是文件资源和网络连接资源。清理函数通常作为第二返回值,参数类型为 func()​。当 Provider​ 中的任何一个拥有清理函数,Injector​ 的函数返回值中也必须包含该函数。并且 Wire​ 对 Provider 的返回值个数及顺序有以下限制:,更多用法具体可以参考 wire官方指南:https://github.com/google/wire/blob/main/docs/guide.md,接着我们就用上述的这些 wire​ 高级特性对 project 服务进行代码改造:,project_dal.go,project_service.go,service.go,handler 伪代码如下:,injector.go 伪代码如下:,wire.go,wire 不允许不同的注入对象拥有相同的类型。google 官方认为这种情况,是设计上的缺陷。这种情况下,可以通过类型别名来将对象的类型进行区分。,例如服务会同时操作两个 Redis 实例,RedisA & RedisB,对于这种情况,wire 无法推导依赖的关系。可以这样进行实现:,依赖注入的本质是用单例来绑定接口和实现接口对象间的映射关系。而通常实践中不可避免的有些对象是有状态的,同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。针对这种场景我们通常设计多层的 DI 容器来实现单例隔离,亦或是脱离 DI 容器自行管理对象的生命周期。,Wire 是一个强大的依赖注入工具。与 Inject 、Dig 等不同的是,Wire只生成代码而不是使用反射在运行时注入,不用担心会有性能损耗。项目工程化过程中,Wire 可以很好协助我们完成复杂对象的构建组装。,更多关于 Wire 的介绍请传送至:https://github.com/google/wire

© 版权声明

相关文章