Golang 官方依赖注入工具wire示例详解

目录
  • 依赖注入是什么
  • 开源选型
  • wire
    • providers
    • injectors
  • 类型区分
  • 总结

依赖注入是什么

Dependency Injection is the idea that your components (usually structs in go) should receive their dependencies when being created.

在 Golang 中,构造一个结构体常见的有两种方式:

  • 在结构体初始化过程中,构建它的依赖;
  • 将依赖作为构造器入参,传入进来。

所谓依赖注入就是第二种思想。不夸张的说,依赖注入是保持我们的软件系统松耦合,可维护的最重要的设计原则。

为什么?

因为当你的依赖通过入参传入,意味着从本对象的角度,你不用去关心它的生成,只用关心它的能力。更具体来讲,它能让我们更加倾向于定义好接口,以接口方法来进行交互。而不是依赖一个具体的实现。

由此而来的另一个好处在于测试。由于依赖是传入的,你的系统只管用它的能力,那么具体这个能力如何实现,其实是由上层来控制的。我们就可以很方便地进行 mock,调整各个场景下依赖的实现,来验证我们的 SUT 的表现。

开源选型

Golang 社区中实现依赖注入的框架有很多,常用的主要是 google/wire, facebook/inject, uber/dig, uber/fx 等,我们这个专栏此前就介绍过 goioc/di,大家感兴趣的话可以往前翻一下。

大体上看,分为两个派系:

  • 代码生成 codegen
  • 基于反射 reflect

其实不光是 DI 工具,针对 Golang 这种强类型,但泛型能力较弱的语言,包括 copier,orm 这类通用框架都会倾向于在这两个路径上二选一。

同样的,DI 也存在这两个排序,上面我们列举的选项中,facebook/inject, uber/dig, uber/fx,以及我们此前介绍的 goioc/di 都采用了基于反射的解法。这样的好处在于使用起来相对直接,不需要额外生成代码。但劣势也是相对的,失去了编译器检查的能力,如果注入有问题,只能在运行时报错,启动时会存在一些性能消耗。

google/wire 是 Google 官方提出的解决方案,也是业界目前最经典的基于 codegen 来解决依赖注入的开源库。相较于反射这种在运行时搞事情的操作,wire 需要开发者提前使用代码生成工具,触发依赖注入代码的生成,在编译器干活。相对的,会稍微麻烦点,但语义更清晰,也消除了运行时的成本。

今天我们就来看看 wire 是怎么用的。

wire

Wire is a code generation tool that automates connecting components using dependency injection. Dependencies between components are represented in Wire as function parameters, encouraging explicit initialization instead of global variables. Because Wire operates without runtime state or reflection, code written to be used with Wire is useful even for hand-written initialization.

wire 在设计上受到了 Java’s Dagger 2 的启发。正如官方对它的定位,wire 是一个 Compile-time Dependency Injection for Go (编译期依赖注入)的代码生成工具。wire 非常的轻量级,只会帮助开发者进行按需初始化。

你甚至可以用手写的初始化代码来替换它,wire 作为一个代码生成工具,仅仅是帮助我们减少注入依赖的繁琐工作。

一个经典的 DI 函数签名类似下面这样:

// NewUserStore returns a UserStore that uses cfg and db as dependencies.
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}

我们需要生成一个 UserStore,所以需要从函数入参中,获取 Config 配置,以及一个 MySQL 的 DB 连接。

思考一下,其实创建对象无非是两种情况:

  • 没有额外依赖,在当前场景下可以直接创建对象;
  • 存在外部依赖,我们需要先对外部依赖进行构建,然后作为参数传进来,进而构建当前对象。

所以,要调用这个 NewUserStore,我们先构建两个依赖。如果 cfg 和 db 都是第一种情况这种简单对象,其实我们手写就够了。

但在生产环境大型应用中,依赖树的构建可能是极其复杂的。A 依赖 B,B 依赖 C 和 D,C 又依赖 E,这个链路可能很长。这意味着如果手写,你的初始化代码会非常冗余,而且很可能要注意初始化顺序。

而且有的依赖可能不仅仅在某一个父对象中使用,而是在多个对象中共用。这个过程是非常痛苦的。一句话:

In practice, making changes to initialization code in applications with large dependency graphs is tedious and slow.

那 wire 干的是一件什么事呢?

wire 希望帮助我们搞清楚,到底我要构建的这些对象,存在哪些依赖,如何一步步构建出来,保证每个对象都能得到它需要的依赖。你不需要考虑这些事情了。

如果要调整一个对象的依赖,我们直接把它的构造器从 wire 模板中增加或删除,或者调整函数签名即可,让 wire 自己去搞清楚,怎么让整个 dependency graph 完整。

wire 的设计中,需要开发者理解两个概念:providers,injectors。下面我们分别来看看。

providers

Providers 就是我们常说的构造器,它们就是一些 Golang 函数,基于一些依赖参数(也可以没有),来构造出来对象。我们经常用的 NewXXX() XXX 就是经典的 Provider,下面是三个例子:

// NewUserStore is the same function we saw above; it is a provider for UserStore,
// with dependencies on *Config and *mysql.DB.
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}
// NewDefaultConfig is a provider for *Config, with no dependencies.
func NewDefaultConfig() *Config {...}
// NewDB is a provider for *mysql.DB based on some connection info.
func NewDB(info *ConnectionInfo) (*mysql.DB, error) {...}

实际上,我们可能会需要提供非常多 Provider,毕竟一个大型项目中涉及的依赖量级是很大的。所以 wire 提供了 ProviderSet 的概念,用来聚合一组 Provider。拿上面 UserStore 来举例,我们可以这样:

var UserStoreSet = wire.ProviderSet(NewUserStore, NewDefaultConfig)

injectors

injectors 也代表了一类函数,和 provider 提供构造器不同,它要做的事情在于实际去注入依赖。

什么?不是说好了 wire 帮我们搞么?怎么还要我们自己写 injector ?

不要慌,的确是 wire 来做,但 wire 需要我们的帮助才能做到这一点。我们总得告诉 wire 我们想要啥样的 injector 签名吧?遇见错误返回不?要用哪些 provider?

要知道,provider 可不仅仅包括那些简单的构造函数,有些对象构造的时候需要别的依赖作为参数,它们自己的构造器也是 provider。我们只有告诉 wire 有哪些 provider,它才能知道要给哪些对象进行构造。

所以,我们需要在这里做好两件事:

  • 明确 injector 的函数签名,确定好入参;
  • 调用 wire.Build,传入一系列 provider(或者 providerSet),wire 将会以此来构造最终结果。
func initUserStore() (*UserStore, error) {
    // We're going to get an error, because NewDB requires a *ConnectionInfo
    // and we didn't provide one.
    wire.Build(UserStoreSet, NewDB)
    return nil, nil  // These return values are ignored.
}

看看示例,发现了么?

除了这两步我们什么都不用干,甚至直接 return 了两个 nil。不要慌,这个函数不是最后要用的,wire 会忽略它的返回值,只需要签名,以及 wire.Build 这个信息。最终我们使用的 injector 并不是自己写的这个。

好,下来操练一下,首先我们安装一下 wire 工具:

go install github.com/google/wire/cmd/wire@latest

安装结束后,直接在当前目录运行 wire 即可。输出如下信息:

$ wire
wire.go:2:10: inject initUserStore: no provider found for ConnectionInfo (required by provider of *mysql.DB)
wire: generate failed

这里信息很明确,上面我们的 func NewDB(info *ConnectionInfo) (*mysql.DB, error) {...} 要求传入 ConnectionInfo,但是我们调用 wire.Build 里面没有对应的 Provider,所以无法生成。

这里我们有两种方案:

  • 加上 ConnectionInfo 依赖作为参数,表明我们这个构造器,就得显式传入;
  • 加上 Provider。

我们试试第一种:

func initUserStore(info ConnectionInfo) (*UserStore, error) {
    wire.Build(UserStoreSet, NewDB)
    return nil, nil  // These return values are ignored.
}

只是加了个入参,看看 wire 能不能识别出来。再次触发命令,会发现目录下多了个 wire_gen.go

// 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 全都搞定了。这里的签名和我们预期的也一样。

这里也能看到,wire 其实非常轻量级,只是把原本需要开发者手写的构建流程,自动生成了。依赖越多,它的作用就越大。

有了生成的代码,我们就可以继续自己的初始化流程,wire 就是个缩减大家人工的小帮手。

类型区分

wire不允许不同的组件拥有相同的类型。官方认为这是设计上的缺陷。我们可以通过类型别名来将组件的类型进行区分。例如服务会同时操作两个Redis,redisA, redisB,不要用这样,wire 无法推导出依赖关系:

func NewRedisA() *goredis.Client {...}
func NewRedisB() *goredis.Client {...}

建议用:

type RedicCliA *goredis.Client
type RedicCliB *goredis.Client
func NewRedisA() RedicCliA {...}
func NewRedisB() RedicCliB {...}

总结

这一篇我们只是从理念和基础用法上带大家初步理解 wire 的定位,更多用法可以参照官方的 tutorial

使用 wire 可以把性能消耗收敛在编译期,但随之而来的代价就是需要编写wire.go文件,生成wire_gen.go,且需要为所有struct编写构造函数,而且需要学习wire.go的写法。

以上就是Golang 官方依赖注入工具wire示例详解的详细内容,更多关于Golang 依赖注入wire的资料请关注我们其它相关文章!

(0)

相关推荐

  • Go依赖注入DI工具wire使用详解(golang常用库包)

    目录 什么是依赖注入 第一次编写mysql操作类: 第二次编写mysql操作类: 第三次编写mysql操作类: 何时使用依赖注入 wire 概念说明 provider 和 injector provider injector wire 使用 快速开始 小结 绑定接口 Provider Set 参考 google 出品的依赖注入库 wire:https://github.com/google/wire 什么是依赖注入 依赖注入 ,英文全名是 dependency injection,简写为 DI.

  • 浅析golang的依赖注入

    目录 前言 基于反射的DI 基于代码生成的DI 前言 如果是做web开发,对依赖注入肯定不陌生,java程序员早就习惯了spring提供的依赖注入,做业务开发时非常方便,只关注业务逻辑即可,对象之间的依赖关系都交给框架. golang是强类型语言,编译后是机器码,所以一般使用 反射 或 代码生成 解决依赖注入的问题 基于反射的DI 基于反射解决DI问题的框架, 使用比较多的是Uber的 dig 库 官方的例子: type Config struct { Prefix string } //初始化

  • golang不到30行代码实现依赖注入的方法

    本文介绍了golang不到30行代码实现依赖注入的方法,分享给大家,具体如下: 项目地址 go-di-demo 本项目依赖 使用标准库实现,无额外依赖 依赖注入的优势 用java的人对于spring框架一定不会陌生,spring核心就是一个IoC(控制反转/依赖注入)容器,带来一个很大的优势是解耦.一般只依赖容器,而不依赖具体的类,当你的类有修改时,最多需要改动一下容器相关代码,业务代码并不受影响. golang的依赖注入原理 总的来说和java的差不多,步骤如下:(golang不支持动态创建对

  • Golang 官方依赖注入工具wire示例详解

    目录 依赖注入是什么 开源选型 wire providers injectors 类型区分 总结 依赖注入是什么 Dependency Injection is the idea that your components (usually structs in go) should receive their dependencies when being created. 在 Golang 中,构造一个结构体常见的有两种方式: 在结构体初始化过程中,构建它的依赖: 将依赖作为构造器入参,传入进

  • Go语言官方依赖注入工具Wire的使用教程

    目录 1. 前言 2. 依赖注入(DI)是什么 3. Wire Come 3.1 简介 3.2 快速使用 3.3 基础概念 4. Wire使用实践 4.1 基础使用 4.2 高级特性 4.3 高阶使用 5. 注意事项 5.1 相同类型问题 5.2 单例问题 6. 结语 1. 前言 接触 Golang 有一段时间了,发现 Golang 同样需要类似 Java 中 Spring 一样的依赖注入框架.如果项目规模比较小,是否有依赖注入框架问题不大,但当项目变大之后,有一个合适的依赖注入框架是十分必要的

  • C#面向对象编程中依赖反转原则的示例详解

    在面向对象编程中,SOLID 是五个设计原则的首字母缩写,旨在使软件设计更易于理解.灵活和可维护.这些原则是由美国软件工程师和讲师罗伯特·C·马丁(Robert Cecil Martin)提出的许多原则的子集,在他2000年的论文<设计原则与设计模式>中首次提出. SOLID 原则包含: S:单一功能原则(single-responsibility principle) O:开闭原则(open-closed principle) L:里氏替换原则(Liskov substitution pri

  • Golang中的错误处理的示例详解

    目录 1.panic 2.包装错误 3.错误类型判断 4.错误值判断 1.panic 当我们执行panic的时候会结束下面的流程: package main import "fmt" func main() { fmt.Println("hello") panic("stop") fmt.Println("world") } 输出: go run 9.go hellopanic: stop 但是panic也是可以捕获的,我们可

  • typescript nodejs 依赖注入实现方法代码详解

    依赖注入通常也是我们所说的ioc模式,今天分享的是用typescript语言实现的ioc模式,这边用到的主要组件是 reflect-metadata 这个组件可以获取或者设置元数据信息,它的作用是拿到原数据后进行对象创建类似C#中的反射,先看第一段代码: import "reflect-metadata"; /** * 对象管理器 */ const _partialContainer = new Map<string, any>(); const PARAMTYPES =

  • golang gorm更新日志执行SQL示例详解

    目录 1. 更新日志 1.1. v1.0 1.1.1. 破坏性变更 gorm执行sql 1. 更新日志 1.1. v1.0 1.1.1. 破坏性变更 gorm.Open返回类型为*gorm.DB而不是gorm.DB 更新只会更新更改的字段 大多数应用程序不会受到影响,只有当您更改回调中的更新值(如BeforeSave,BeforeUpdate)时,应该使用scope.SetColumn,例如: func (user *User) BeforeUpdate(scope *gorm.Scope) {

  • Golang 实现 RTP音视频传输示例详解

    目录 引言 RTP 数据包头部字段 Golang 的相关实现 结尾 引言 在 Coding 之前我们先来简单介绍一下 RTP(Real-time Transport Protocol), 正如它的名字所说,用于互联网的实时传输协议,通过 IP 网络传输音频和视频的网络协议. 由音视频传输工作小组开发,1996 年首次发布,并提出了以下使用设想. 简单的多播音频会议 使用 IP 的多播服务进行语音通信.通过某种分配机制,获取多播组地址和端口对.一个端口用于音频数据的,另一个用于控制(RTCP)包,

  • Golang WorkerPool线程池并发模式示例详解

    目录 正文 处理CVS文件记录 获取测试数据 线程池耗时差异 正文 Worker Pools 线程池是一种并发模式.该模式中维护了固定数量的多个工作器,这些工作器等待着管理者分配可并发执行的任务.该模式避免了短时间任务创建和销毁线程的代价. 在 golang 中,我们使用 goroutine 和 channel 来构建这种模式.工作器 worker 由一个 goroutine 定义,该 goroutine 通过 channel 获取数据. 处理CVS文件记录 接下来让我们通过一个例子,来进一步理

  • Awaitility同步异步工具实战示例详解

    目录 引言 1. awaitility入门 1.1 静态导入 1.2 简单例子 2. awaitility在RocketMQ中的实战 3. 总结 引言 在编写测试用例的时候遇到有异步或者队列处理的时候经常会用到 Thread.sleep() 等待来进行测试.例如:DLedger 测试选举的过程.当DLedger Leader下线.此时DLedger会重新发起选举,这个选举的过程是需要一定时间.很多时候在测试代码中就会使用 Thread.sleep . 由于选举需要的时间多少不确定所以sleep时

随机推荐