天津网站建设费用,重庆模板网站建设费用,地方门户网站备案,外贸建站 台州在现代的 web 框架里面#xff0c;基本都有实现了依赖注入的功能#xff0c;可以让我们很方便地对应用的依赖进行管理#xff0c;同时免去在各个地方 new 对象的麻烦。比如 Laravel 里面的 Application#xff0c;又或者 Java 的 Spring 框架也自带依赖注入功能。
今天我们…在现代的 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 里面我们可以通过 ServiceProvider 的 app()-register() 或者 app()-bind() 等函数来做依赖注入的一些操作。
inject 依赖注入模型/设计
以下是 Injector 的大概模型Injector 接口里面嵌套了 Applicator、Invoker、TypeMapper 接口之所以这样做是出于接口隔离原则考虑因为这三者代表了细化的三种不同功能分离出不同的接口可以让我们的代码更加的清晰也会更利于代码的后续演进。
Injector依赖注入容器Applicator结构体注入的接口Invoker使用注入的依赖来调用函数TypeMapper类型映射需要特别注意的是在 Injector 里面是通过类型来绑定依赖不同于 Laravel 的依赖注入容器可以通过字符串命名的方式来绑定依赖当然将 Injector 稍微改改也是可以实现的就看有没有这种需求罢了。
// 依赖注入容器
type Injector interface {ApplicatorInvokerTypeMapper// 上一级 InjectorSetParent(Injector)
}// 给结构体字段注入依赖
type Applicator interface {Apply(interface{}) error
}// 调用函数Invoke 的参数是被调用的函数
// 这个函数的参数事先通过 Injector 注入
// 调用的时候从 Injector 里面获取依赖
type Invoker interface {Invoke(interface{}) ([]reflect.Value, error)
}// 往 Injector 注入依赖
type TypeMapper interface {Map(...interface{}) TypeMapperMapTo(interface{}, interface{}) TypeMapperSet(reflect.Type, reflect.Value) TypeMapperValue(reflect.Type) reflect.Value
}表示成图像大概如下 我们可以通过 Injector 的 TypeMapper 来往依赖注入容器里面注入依赖然后在我们需要为结构体的字段注入依赖又或者为函数参数注入依赖的时候可以通过 Applicator 或者 Invoker 来实现注入依赖。
而 SetParent 这个方法比较有意思它其实将 Injector 这个模型拓展了形成了一个有父子关系的模型。在其他语言里面可能作用不是很明显但是在 go 里面这个父子模型恰好和 go 的协程的父子模型一致。在 go 里面我们可以在一个协程里面再创建一个 Injector然后在这里面定义一些在当前协程以及当前协程子协程可以用到的一些依赖而不用影响外部的 Injector。
当然上面说到的协程只是 Injector 里面 SetParent 的一种用法另外一种用法是我们的 web 应用往往会根据路由前缀来划分为不同的组而这种路由组的结构组织方式其实也是一种父子结构在这种场景下我们就可以针对全局注入一些依赖的情况下再针对某个路由组来注入路由组特定的依赖。
injector 的依赖注入实现
我们来看看 injector 的结构体
type injector struct {// 注入的依赖values map[reflect.Type]reflect.Value// 上级 Injectorparent Injector
}这个结构体定义很简单就只有两个字段values 和 parent我们通过 TypeMapper 注入的依赖都保存在 values 里面values 是通过反射来记录我们注入的参数类型和值的。
那我们是如何注入依赖的呢再来看看 TypeMapper 的 Map 方法
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 的值。其他的两个方法 MapTo、Set 也是类似的功能最终的效果都是获取依赖的类型作为 values 的 key依赖的值作为 values 的值。
到此为止我们知道 Injector 是如何注入依赖的了。
那么它又是如何去从依赖注入容器里面拿到我们注入的数据的呢又是如何使用这些数据的呢
我们再来看看 callInvoke 方法也就是 Injector 的 Invoke 实现
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.Typevar 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)
// 调用函数 testtest 的参数 C 会通过依赖注入容器获取
_, _ inj.Invoke(test)
// 输出 c called这个例子中我们通过 inj.Map 来注入了依赖在后续通过 inj.Invoke 来调用 test 函数的时候将会从依赖注入容器里面获取 test 的参数然后将这些参数传入 test 来调用。
这个例子也许比较简单但是如果我们很多地方都需要用到 C 这个参数的话我们通过 inj.Invoke 的方式来调用函数就可以避免每一次调用都要实例化 C 的繁琐操作了。