go 依赖注入设计与实现

在现代的 web 框架里面,基本都有实现了依赖注入的功能,可以让我们很方便地对应用的依赖进行管理,同时免去在各个地方 new 对象的麻烦。比如 Laravel 里面的 Application,又或者 Java 的 Spring 框架也自带依赖注入功能。

今天我们来看看 go 里面实现依赖注入的一种方式,以 inject 库为例子(https://github.com/flamego/flamego/tree/main/inject)。

我们要了解一个软件的设计,先要看它定义了一个什么样的模型,但是在了解模型之前,我们更应该清楚了解,为什么会出现这个模型,也就是我们构建出了这个模型到底是为了解决什么问题。

依赖注入要解决的问题

我们先来看看,在没有依赖注入之前,我们需要的依赖是如何构建出来的,假设有如下 struct 定义:

type A struct {
}

type B struct {
	a A
}

type C struct {
	b B
}

func test(c C) {
    println("c called")
}

假设我们要调用 test,就需要创建一个 C 的实例,而创建 C 的实例需要创建一个 B 的实例,而创建 B 的实例需要一个 A 的实例。如下是一个例子:

a := A{}
b := B{a: a}
c := C{b: b}
test(c)

我们可以看到,这个过程非常的繁琐,只有一个地方需要这样调用 test 还好,如果有多个地方都需要调用 test,那我们就要做很多创建实例的操作,而且一旦实例的构建过程发生变化,我们就需要改动很多地方

所以现在的 web 框架里面一般都将这个实例化的过程固化下来,在框架的某个地方注册一些实例化的函数,在我们需要的时候就调用之前注册的实例化的函数,实例化之后,再根据需要看看是否需要将这个实例保留在内存里面,从而在免去了手动实例化的过程之外,节省我们资源的开销(不用每次使用的时候都实例化一次)。

而这里说到的固化的实例化过程,其实就是我们本文所说的依赖注入。在 Laravel 里面我们可以通过 ServiceProviderapp()->register() 或者 app()->bind() 等函数来做依赖注入的一些操作。

inject 依赖注入模型/设计

以下是 Injector 的大概模型,Injector 接口里面嵌套了 ApplicatorInvokerTypeMapper 接口,之所以这样做是出于接口隔离原则考虑,因为这三者代表了细化的三种不同功能,分离出不同的接口可以让我们的代码更加的清晰,也会更利于代码的后续演进。

  • Injector:依赖注入容器
  • Applicator:结构体注入的接口
  • Invoker:使用注入的依赖来调用函数
  • TypeMapper:类型映射,需要特别注意的是,在 Injector 里面,是通过类型来绑定依赖(不同于 Laravel 的依赖注入容器可以通过字符串命名的方式来绑定依赖,当然将 Injector 稍微改改也是可以实现的,就看有没有这种需求罢了)。
// 依赖注入容器
type Injector interface {
	Applicator
	Invoker
	TypeMapper
    // 上一级 Injector
	SetParent(Injector)
}

// 给结构体字段注入依赖
type Applicator interface {
	Apply(interface{}) error
}

// 调用函数,Invoke 的参数是被调用的函数,
// 这个函数的参数事先通过 Injector 注入,
// 调用的时候从 Injector 里面获取依赖
type Invoker interface {
	Invoke(interface{}) ([]reflect.Value, error)
}

// 往 Injector 注入依赖
type TypeMapper interface {
	Map(...interface{}) TypeMapper
	MapTo(interface{}, interface{}) TypeMapper
	Set(reflect.Type, reflect.Value) TypeMapper
	Value(reflect.Type) reflect.Value
}

表示成图像大概如下:

在这里插入图片描述

我们可以通过 InjectorTypeMapper 来往依赖注入容器里面注入依赖,然后在我们需要为结构体的字段注入依赖,又或者为函数参数注入依赖的时候,可以通过 Applicator 或者 Invoker 来实现注入依赖。

SetParent 这个方法比较有意思,它其实将 Injector 这个模型拓展了,形成了一个有父子关系的模型。在其他语言里面可能作用不是很明显,但是在 go 里面,这个父子模型恰好和 go 的协程的父子模型一致。在 go 里面,我们可以在一个协程里面再创建一个 Injector,然后在这里面定义一些在当前协程以及当前协程子协程可以用到的一些依赖,而不用影响外部的 Injector

当然上面说到的协程只是 Injector 里面 SetParent 的一种用法,另外一种用法是,我们的 web 应用往往会根据路由前缀来划分为不同的组,而这种路由组的结构组织方式其实也是一种父子结构,在这种场景下,我们就可以针对全局注入一些依赖的情况下,再针对某个路由组来注入路由组特定的依赖。

injector 的依赖注入实现

我们来看看 injector 的结构体:

type injector struct {
    // 注入的依赖
	values map[reflect.Type]reflect.Value
    // 上级 Injector
	parent Injector
}

这个结构体定义很简单,就只有两个字段,valuesparent,我们通过 TypeMapper 注入的依赖都保存在 values 里面,values 是通过反射来记录我们注入的参数类型和值的。

那我们是如何注入依赖的呢?再来看看 TypeMapperMap 方法:

func (inj *injector) Map(values ...interface{}) TypeMapper {
	for _, val := range values {
		inj.values[reflect.TypeOf(val)] = reflect.ValueOf(val)
	}
	return inj
}

我们可以看到,对于传入给 Map 的参数,这里获取了它的反射类型作为 values map 的 key,而获取了传入参数的反射值作为 values 里面 map 的值。其他的两个方法 MapToSet 也是类似的功能,最终的效果都是获取依赖的类型作为 values 的 key,依赖的值作为 values 的值

到此为止,我们知道 Injector 是如何注入依赖的了。

那么它又是如何去从依赖注入容器里面拿到我们注入的数据的呢?又是如何使用这些数据的呢?

我们再来看看 callInvoke 方法(也就是 InjectorInvoke 实现):

func (inj *injector) callInvoke(f interface{}, t reflect.Type, numIn int) ([]reflect.Value, error) {
    // 参数切片,用来保存从 Injector 里面获取的依赖
	var in []reflect.Value
    // 只有 f 有参数的时候,才需要从 Injector 获取依赖
	if numIn > 0 {
        // 初始化切片
		in = make([]reflect.Value, numIn)
		var argType reflect.Type
		var val reflect.Value
        // 遍历 f 参数
		for i := 0; i < numIn; i++ {
            // 获取 f 参数类型
			argType = t.In(i)
            // 从 Injector 获取该类型对应的依赖
			val = inj.Value(argType)
            // 如果函数参数未注入,则调用出错
			if !val.IsValid() {
				return nil, fmt.Errorf("value not found for type %v", argType)
			}

            // 保存从 Injector 获取到的值
			in[i] = val
		}
	}
    // 通过反射调用 f 函数,in 是参数切片
	return reflect.ValueOf(f).Call(in), nil
}

参数和返回值说明:

  • 第一个参数是我们 Invoke 的函数,这个函数的参数,都会通过 Injector 根据函数参数类型获取
  • 第二个参数 f 的反射类型,也就是 reflect.TypeOf(f)
  • 第三个参数是 f 的参数个数
  • 返回值是 reflect.Value 切片,如果我们在调用过程出错,返回 error

在这个函数中,会通过反射来获取 f 的参数类型(reflect.Type),拿到这个类型之后,从 Injector 里面获取我们之前注入的依赖,这样我们就可以拿到所有参数对应的值。最后,通过 reflect.ValueOf(f) 来调用 f 函数,参数是我们从 Injector 获取到的值的切片。调用之后,返回函数调用结果,一个 reflect.Value 切片。

当然,这只是其中一种使用依赖的方式,另外一种方式也比较常见,就是为结构体注入依赖,这跟 hyperf 里面通过注释注解又或者 Spring 里面的注入方式有点类似。在 Injector 里面是通过 Apply 来为结构体字段注入依赖的:

// 参数 val 是待注入依赖的结构体
func (inj *injector) Apply(val interface{}) error {
	v := reflect.ValueOf(val)

    // 获取底层元素
	for v.Kind() == reflect.Ptr {
		v = v.Elem()
	}

    // 底层类型不是结构体则返回
	if v.Kind() != reflect.Struct {
		return nil // Should not panic here ?
	}

    // v 的反射类型
	t := v.Type()

    // 遍历结构体的字段
	for i := 0; i < v.NumField(); i++ {
        // 获取第 i 个结构体字段
        // v 的类型是 reflect.Value
        // v.Field 返回的是结构体字段的值
		f := v.Field(i)
        // t 的类型是 *reflect.rtype
        // t.Field 返回的是 reflect.Type,是类型信息
		structField := t.Field(i)
        // 检查是否有 inject tag,有这个 tag 才会进行依赖注入
		_, ok := structField.Tag.Lookup("inject")
        // 字段支持反射设置,并且存在 inject tag 才会进行注入
		if f.CanSet() && ok {
            // 通过反射类型从 Injector 中获取对应的值
			ft := f.Type()
			v := inj.Value(ft)
            // 获取不到注入的依赖,则返回错误
			if !v.IsValid() {
				return fmt.Errorf("value not found for type %v", ft)
			}

            // 设置结构体字段值
			f.Set(v)
		}

	}
	return nil
}

简单来说,Injector 里面,通过 TypeMapper 来注入依赖,然后通过 Apply 或者 Invoke 来使用注入的依赖。

例子

还是以一开始的例子为例,通过依赖注入的方式来改造一下:

a := A{}
b := B{a: a}
c := C{b: b}

// 新建依赖注入容器
inj := injector{
    values: make(map[reflect.Type]reflect.Value),
}
// 注入依赖 c
inj.Map(c)
// 调用函数 test,test 的参数 `C` 会通过依赖注入容器获取
_, _ = inj.Invoke(test)
// 输出 "c called"

这个例子中,我们通过 inj.Map 来注入了依赖,在后续通过 inj.Invoke 来调用 test 函数的时候,将会从依赖注入容器里面获取 test 的参数,然后将这些参数传入 test 来调用。

这个例子也许比较简单,但是如果我们很多地方都需要用到 C 这个参数的话,我们通过 inj.Invoke 的方式来调用函数就可以避免每一次调用都要实例化 C 的繁琐操作了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/343858.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【ASOC全解析(一)】ASOC架构简介和欲解决的问题

【ASOC全解析&#xff08;一&#xff09;】ASOC架构简介和欲解决的问题 一、什么是ASOC以及ASOC解决的三个问题二、ASOC的组成与功能解决第一个问题解决第二个问题解决第三个问题 三、ASOC基本工作原理 /********************************************************************…

Parade Series - Android Studio

硬件支持 CPU i7 RAM 16Gb -------------- ------- Java 3Gb Android 33GbJava Enviroment C:\ ├─ Java │ ├─ jdk1.8.0_181 │ ├─ jre1.8.0_181 │ ├─ maven-3.8.5 │ └─ gradle-6.5 └─ Cache├─ gr…

基于中文垃圾短信数据集的经典文本分类算法实现

垃圾短信的泛滥给人们的日常生活带来了严重干扰&#xff0c;其中诈骗短信更是威胁到人们的信息与财产安全。因此&#xff0c;研究如何构建一种自动拦截过滤垃圾短信的机制有较强的实际应用价值。本文基于中文垃圾短信数据集&#xff0c;分别对比了朴素贝叶斯、逻辑回归、随机森…

哈希--73. 矩阵置零/medium 理解度A

73. 矩阵置零 1、题目2、题目分析3、复杂度最优解代码示例4、适用场景 1、题目 给定一个 m x n 的矩阵&#xff0c;如果一个元素为 0 &#xff0c;则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,1,1],[1,0,1],[1,…

Overleaf(LaTeX文档在线编写平台)使用学习记录

一、LaTeX简概[1] LaTeX&#xff0c;是一种基于TEX的排版系统&#xff0c;是一种可以处理排版和渲染的标记语言。由美国计算机科学家莱斯利兰伯特在20世纪80年代初期开发&#xff0c;利用这种格式系统的处理&#xff0c;即使用户没有排版和程序设计的知识也可以充分发挥由TEX所…

CACTER邮件安全网关独家安全解决方案——保障企业邮件系统安全

随着科技的不断发展&#xff0c;网络攻击技术也在不断演变&#xff0c;尤其是在电子邮件领域&#xff0c;各种高级变种威胁层出不穷&#xff0c;比如定制化的钓鱼邮件和带有高级恶意软件的邮件等。这些威胁邮件往往能够绕过传统的安全防护措施&#xff0c;包括反垃圾邮件、反钓…

软件安全测试的重要性简析,专业安全测试报告如何申请?

在当今数字化时代&#xff0c;软件在我们的日常生活中扮演着至关重要的角色&#xff0c;但也带来了各种潜在的安全威胁。为了保障用户的信息安全和维护软件的可靠性&#xff0c;软件安全测试显得尤为重要。 软件安全测试是指通过一系列的方法和技术&#xff0c;对软件系统中的…

pikachu_csrf通关攻略

csrf&#xff08;get&#xff09; 打开pikachu靶场&#xff1a; 1. 根据提示给的账户密码进行登录 2. 打开代理拦截数据包将拦截数据发送到已打开的burp中&#xff1a; 修改数据进行发包&#xff1a; 从上面的url可见&#xff0c;修改用户信息的时候&#xff0c;是不带任何不…

性能优化(CPU优化技术)-NEON指令介绍

「发表于知乎专栏《移动端算法优化》」 本文主要介绍了 NEON 指令相关的知识&#xff0c;首先通过讲解 arm 指令集的分类&#xff0c;NEON寄存器的类型&#xff0c;树立基本概念。然后进一步梳理了 NEON 汇编以及 intrinsics 指令的格式。最后结合指令的分类&#xff0c;使用例…

如何基于 ESP32 芯片测试 WiFi 连接距离、获取连接的 AP 信号强度(RSSI)以及 WiFi吞吐测试

测试说明&#xff1a; 测试 WiFi 连接距离&#xff0c;是将 ESP32 作为 WiFi Station 模式来连接路由器&#xff0c;通过在开阔环境下进行拉距来测试。另外&#xff0c;可以通过增大 WiFi TX Power 来增大连接距离。 获取连接的 AP 信号强度&#xff0c;一般可以通过 WiFi 扫描…

机器学习_从线性回归到逻辑回归原理和实战

文章目录 介绍分类问题用线性回归阶跃函数完成分类通过 Sigmiod 函数进行转换逻辑回归的假设函数逻辑回归的损失函数用逻辑回归解决二元分类问题 介绍分类问题 机器学习两个主要应用是回归和分类问题。 逻辑回归算法的本质其实仍然是回归。这个算法也是通过调整权重w和偏置b来…

GBASE南大通用提供给.NET 应用程序访问 GBase 数据库、获取数据、管理数据的一套完整的解决方案

GBase ADO.NET&#xff08;全称是 .NET Framework Data Provider For GBase&#xff09;提 供给.NET 应用程序访问 GBase 数据库、获取数据、管理数据的一套完整的解决 方案。 GBase ADO.NET 的四个核心类及若干功能类具有以下功能&#xff1a;  建立和管理与 GBase 数据库连…

java web mvc-04-Apache Wicket

拓展阅读 Spring Web MVC-00-重学 mvc mvc-01-Model-View-Controller 概览 web mvc-03-JFinal web mvc-04-Apache Wicket web mvc-05-JSF JavaServer Faces web mvc-06-play framework intro web mvc-07-Vaadin web mvc-08-Grails 开源 The jdbc pool for java.(java …

21.云原生之ArgoCD CICD实战(部分待补充)

云原生专栏大纲 文章目录 部署项目介绍项目结构介绍GitLab CI/CDGitLab CI/CD主要特点和功能 部署测试argocd的cd过程CICD工作流准备工作github中工作流文件创建gitlab中工作流文件创建【实操待补充】GitLab CI示例 数据加密之seale sealedBitnami Sealed Secrets介绍Bitnami …

11.前端--CSS-背景属性

1.背景颜色 样式名称&#xff1a; background-color 定义元素的背景颜色 使用方式: background-color:颜色值; 其他说明&#xff1a; 元素背景颜色默认值是 transparent&#xff08;透明&#xff09;      background-color:transparent; 代码演示&#xff1a; 背景色…

盖子的c++小课堂:第二十六讲:双向链表

前言 谢谢各位粉丝的支持,望我早日突破1000粉 双向链表 干货!单链表从原理到实现——附python和C++两个版本 - 知乎单链表是链表家族中的一员,每个节点依旧由 数据域(data)和指针域(next)组成,链表的具体概念下面有介绍: 机器学习入坑者:程序员基本功——链表的基…

大数据学习之Flink算子、了解(Source)源算子(基础篇二)

Source源算子&#xff08;基础篇二&#xff09; 目录 Source源算子&#xff08;基础篇二&#xff09; 二、源算子&#xff08;source&#xff09; 1. 准备工作 2.从集合中读取数据 可以使用代码中的fromCollection()方法直接读取列表 也可以使用代码中的fromElements()方…

“探索C语言操作符的神秘世界:从入门到精通的全方位解析“

各位少年&#xff0c;我是博主那一脸阳光&#xff0c;今天来分享深度解析C语言操作符&#xff0c;C语言操作符能帮我们解决很多逻辑性的问题&#xff0c;减少很多代码量&#xff0c;就好比数学的各种符号&#xff0c;我们现在深度解剖一下他们。 前言 在追求爱情的道路上&…

Google ASPIRE框架:赋予大型语言模型(LLMs)自我评估的新动力

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

Vue生命周期;综合案例;工程化开发入门

Vue的生命周期 和 生命周期的四个阶段 思考&#xff1a; 什么时候可以发送初始化渲染请求&#xff1f;&#xff08;越早越好&#xff1a;最早可以早到什么时候&#xff1f;&#xff09; 什么时候可以开始操作dom&#xff1f;&#xff08;至少dom得渲染出来&#xff09; Vue生命…