我的编程空间,编程开发者的网络收藏夹
学习永远不晚

一文带你搞懂Golang依赖注入的设计与实现

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

一文带你搞懂Golang依赖注入的设计与实现

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

今天我们来看看 go 里面实现依赖注入的一种方式,以 flamego 里的 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 的繁琐操作了。

以上就是一文带你搞懂Golang依赖注入的设计与实现的详细内容,更多关于Golang依赖注入的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

一文带你搞懂Golang依赖注入的设计与实现

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

一文带你搞懂Golang依赖注入的设计与实现

在现代的web框架里面,基本都有实现了依赖注入的功能,可以让我们很方便地对应用的依赖进行管理。今天我们来看看go里面实现依赖注入的一种方式,感兴趣的可以了解一下
2023-01-05

一文带你了解Golang中interface的设计与实现

本文就来详细说说为什么说 接口本质是一种自定义类型,以及这种自定义类型是如何构建起 go 的 interface 系统的,感兴趣的小伙伴可以跟随小编一起学习一下
2023-01-04

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录