当前位置: 首页 > 教程 > GO >

Go语言inject库:依赖注入

精华
小牛编辑
199浏览
2023-03-14
在介绍 inject 之前我们先来简单介绍一下“依赖注入”和“控制反转”这两个概念。

正常情况下,对函数或方法的调用是我们的主动直接行为,在调用某个函数之前我们需要清楚地知道被调函数的名称是什么,参数有哪些类型等等。

所谓的控制反转就是将这种主动行为变成间接的行为,我们不用直接调用函数或对象,而是借助框架代码进行间接的调用和初始化,这种行为称作“控制反转”,库和框架能很好的解释控制反转的概念。

依赖注入是实现控制反转的一种方法,如果说控制反转是一种设计思想,那么依赖注入就是这种思想的一种实现,通过注入参数或实例的方式实现控制反转。如果没有特殊说明,我们可以认为依赖注入和控制反转是一个东西。

控制反转的价值在于解耦,有了控制反转就不需要将代码写死,可以让控制反转的的框架代码读取配置,动态的构建对象,这一点在 Java 的 Spring 框架中体现的尤为突出。

inject 实践

inject 是依赖注入的Go语言实现,它能在运行时注入参数,调用方法,是 Martini 框架(Go语言中著名的 Web 框架)的基础核心。

在介绍具体实现之前,先来想一个问题,如何通过一个字符串类型的函数名来调用函数?Go语言没有 Java 中的 Class.forName 方法可以通过类名直接构造对象,所以这种方法是行不通的,能想到的方法就是使用 map 实现一个字符串到函数的映射,示例代码如下:
func fl() {
    println ("fl")
}
func f2 () {
    println ("f2")
}
funcs := make(map[string] func ())
funcs ["fl"] = fl
funcs ["f2"] = fl
funcs ["fl"]()
funcs ["f2"]()
但是这有个缺陷,就是 map 的 Value 类型被写成 func(),不同参数和返回值的类型的函数并不能通用。将 map 的 Value 定义为 interface{} 空接口类型即可以解决该问题,但需要借助类型断言或反射来实现,通过类型断言实现等于又绕回去了,反射是一种可行的办法。

inject 包借助反射实现函数的注入调用,下面通过一个示例来看一下。
package main

import (
    "fmt"
    "github.com/codegangsta/inject"
)

type S1 interface{}
type S2 interface{}

func Format(name string, company S1, level S2, age int) {
    fmt.Printf("name = %s, company=%s, level=%s, age = %d!\n", name, company, level, age)
}
func main() {
    //控制实例的创建
    inj := inject.New()
    //实参注入
    inj.Map("tom")
    inj.MapTo("tencent", (*S1)(nil))
    inj.MapTo("T4", (*S2)(nil))
    inj.Map(23)
    //函数反转调用
    inj.Invoke(Format)
}
运行结果如下:

name = tom, company=tencent, level=T4, age = 23!

可见 inject 提供了一种注入参数调用函数的通用功能,inject.New() 相当于创建了一个控制实例,由其来实现对函数的注入调用。inject 包不但提供了对函数的注入,还实现了对 struct 类型的注入,示例代码如下所示:
package main

import (
    "fmt"
    "github.com/codegangsta/inject"
)

type S1 interface{}
type S2 interface{}
type Staff struct {
    Name    string `inject`
    Company S1     `inject`
    Level   S2     `inject`
    Age     int    `inject`
}

func main() {
    //创建被注入实例
    s := Staff{}
    //控制实例的创建
    inj := inject.New()
    //初始化注入值
    inj.Map("tom")
    inj.MapTo("tencent", (*S1)(nil))
    inj.MapTo("T4", (*S2)(nil))
    inj.Map(23)
    //实现对 struct 注入
    inj.Apply(&s)
    //打印结果
    fmt.Printf("s = %v\n", s)
}
运行结果如下:

s = {tom tencent T4 23}

可以看到 inject 提供了一种对结构类型的通用注入方法。至此,我们仅仅从宏观层面了解 iniect 能做什么,下面从源码实现角度来分析 inject。

inject 原理分析

inject 包中只有 2 个文件,一个是 inject.go 文件和一个 inject_test.go 文件,这里我们只需要关注 inject.go 文件即可。

inject.go 短小精悍,包括注释和空行在内才 157 行代码,代码中定义了 4 个接口,包括一个父接口和三个子接口,如下所示:
type Injector interface {
    Applicator
    Invoker
    TypeMapper
    SetParent(Injector)
}

type Applicator interface {
    Apply(interface{}) error
}

type Invoker interface {
    Invoke(interface{}) ([]reflect.Value, error)
}

type TypeMapper interface {
    Map(interface{}) TypeMapper
    MapTo(interface{}, interface{}) TypeMapper
    Get(reflect.Type) reflect.Value
}
Injector 接口是 Applicator、Invoker、TypeMapper 接口的父接口,所以实现了 Injector 接口的类型,也必然实现了 Applicator、Invoker 和 TypeMapper 接口:
  • Applicator 接口只规定了 Apply 成员,它用于注入 struct。
  • Invoker 接口只规定了 Invoke 成员,它用于执行被调用者。
  • TypeMapper 接口规定了三个成员,Map 和 MapTo 都用于注入参数,但它们有不同的用法,Get 用于调用时获取被注入的参数。

另外 Injector 还规定了 SetParent 行为,它用于设置父 Injector,其实它相当于查找继承。也即通过 Get 方法在获取被注入参数时会一直追溯到 parent,这是个递归过程,直到查找到参数或为 nil 终止。
type injector struct {
    values map[reflect.Type]reflect.Value
    parent Injector
}

func InterfaceOf(value interface{}) reflect.Type {
    t := reflect.TypeOf(value)

    for t.Kind() == reflect.Ptr {
        t = t.Elem()
    }

    if t.Kind() != reflect.Interface {
        panic("Called inject.InterfaceOf with a value that is not a pointer to an interface. (*MyInterface)(nil)")
    }

    return t
}

func New() Injector {
    return &injector{
        values: make(map[reflect.Type]reflect.Value),
    }
}
injector 是 inject 包中唯一定义的 struct,所有的操作都是基于 injector struct 来进行的,它有两个成员 values 和 parent。values 用于保存注入的参数,是一个用 reflect.Type 当键、reflect.Value 为值的 map,理解这点将有助于理解 Map 和 MapTo。

New 方法用于初始化 injector struct,并返回一个指向 injector struct 的指针,但是这个返回值被 Injector 接口包装了。

InterfaceOf 方法虽然只有几句实现代码,但它是 Injector 的核心。InterfaceOf 方法的参数必须是一个接口类型的指针,如果不是则引发 panic。InterfaceOf 方法的返回类型是 reflect.Type,大家应该还记得 injector 的成员 values 就是一个 reflect.Type 类型当键的 map。这个方法的作用其实只是获取参数的类型,而不关心它的值。

示例代码如下所示:
package main

import (
    "fmt"
    "github.com/codegangsta/inject"
)

type SpecialString interface{}

func main() {
    fmt.Println(inject.InterfaceOf((*interface{})(nil)))
    fmt.Println(inject.InterfaceOf((*SpecialString)(nil)))
}
运行结果如下:

interface {}
main.SpecialString

InterfaceOf 方法就是用来得到参数类型,而不关心它具体存储的是什么值。
func (i *injector) Map(val interface{}) TypeMapper {
    i.values[reflect.TypeOf(val)] = reflect.ValueOf(val)
    return i
}

func (i *injector) MapTo(val interface{}, ifacePtr interface{}) TypeMapper {
    i.values[InterfaceOf(ifacePtr)] = reflect.ValueOf(val)
    return i
}

func (i *injector) Get(t reflect.Type) reflect.Value {
    val := i.values[t]
    if !val.IsValid() && i.parent != nil {
        val = i.parent.Get(t)
    }
    return val
}

func (i *injector) SetParent(parent Injector) {
    i.parent = parent
}
Map 和 MapTo 方法都用于注入参数,保存于 injector 的成员 values 中。这两个方法的功能完全相同,唯一的区别就是 Map 方法用参数值本身的类型当键,而 MapTo 方法有一个额外的参数可以指定特定的类型当键。但是 MapTo 方法的第二个参数 ifacePtr 必须是接口指针类型,因为最终 ifacePtr 会作为 InterfaceOf 方法的参数。

为什么需要有 MapTo 方法?因为注入的参数是存储在一个以类型为键的 map 中,可想而知,当一个函数中有一个以上的参数的类型是一样时,后执行 Map 进行注入的参数将会覆盖前一个通过 Map 注入的参数。

SetParent 方法用于给某个 Injector 指定父 Injector。Get 方法通过 reflect.Type 从 injector 的 values 成员中取出对应的值,它可能会检查是否设置了 parent,直到找到或返回无效的值,最后 Get 方法的返回值会经过 IsValid 方法的校验。

示例代码如下所示:
package main

import (
    "fmt"
    "reflect"
    "github.com/codegangsta/inject"
)

type SpecialString interface{}

func main() {
    inj := inject.New()
    inj.Map("小牛知识库")
    inj.MapTo("Golang", (*SpecialString)(nil))
    inj.Map(20)
    fmt.Println("字符串是否有效?", inj.Get(reflect.TypeOf("Go语言入门教程")).IsValid())
    fmt.Println("特殊字符串是否有效?", inj.Get(inject.InterfaceOf((*SpecialString)(nil))).IsValid())
    fmt.Println("int 是否有效?", inj.Get(reflect.TypeOf(18)).IsValid())
    fmt.Println("[]byte 是否有效?", inj.Get(reflect.TypeOf([]byte("Golang"))).IsValid())
    inj2 := inject.New()
    inj2.Map([]byte("test"))
    inj.SetParent(inj2)
    fmt.Println("[]byte 是否有效?", inj.Get(reflect.TypeOf([]byte("Golang"))).IsValid())
}
运行结果如下所示:

字符串是否有效? true
特殊字符串是否有效? true
int 是否有效? true
[]byte 是否有效? false
[]byte 是否有效? true

通过以上例子应该知道 SetParent 是什么样的行为,是不是很像面向对象中的查找链?
func (inj *injector) Invoke(f interface{}) ([]reflect.Value, error) {
    t := reflect.TypeOf(f)

    var in = make([]reflect.Value, t.NumIn()) //Panic if t is not kind of Func
    for i := 0; i < t.NumIn(); i++ {
        argType := t.In(i)
        val := inj.Get(argType)
        if !val.IsValid() {
            return nil, fmt.Errorf("Value not found for type %v", argType)
        }
        in[i] = val
    }
    return reflect.ValueOf(f).Call(in), nil
}
Invoke 方法用于动态执行函数,当然执行前可以通过 Map 或 MapTo 来注入参数,因为通过 Invoke 执行的函数会取出已注入的参数,然后通过 reflect 包中的 Call 方法来调用。Invoke 接收的参数 f 是一个接口类型,但是 f 的底层类型必须为 func,否则会 panic。
package main

import (
    "fmt"
    "github.com/codegangsta/inject"
)

type SpecialString interface{}

func Say(name string, gender SpecialString, age int) {
    fmt.Printf("My name is %s, gender is %s, age is %d!\n", name, gender, age)
}

func main() {
    inj := inject.New()
    inj.Map("张三")
    inj.MapTo("男", (*SpecialString)(nil))
    inj2 := inject.New()
    inj2.Map(25)
    inj.SetParent(inj2)
    inj.Invoke(Say)
}
运行结果如下:

My name is 张三, gender is 男, age is 25!

上面的例子如果没有定义 SpecialString 接口作为 gender 参数的类型,而把 name 和 gender 都定义为 string 类型,那么 gender 会覆盖 name 的值。
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
    }

    t := v.Type()

    for i := 0; i < v.NumField(); i++ {
        f := v.Field(i)
        structField := t.Field(i)
        if f.CanSet() && structField.Tag == "inject" {
            ft := f.Type()
            v := inj.Get(ft)
            if !v.IsValid() {
                return fmt.Errorf("Value not found for type %v", ft)
            }
            f.Set(v)
        }
    }
    return nil
}
Apply 方法是用于对 struct 的字段进行注入,参数为指向底层类型为结构体的指针。可注入的前提是:字段必须是导出的(也即字段名以大写字母开头),并且此字段的 tag 设置为 `inject`

示例代码如下所示:
package main

import (
    "fmt"
    "github.com/codegangsta/inject"
)

type SpecialString interface{}

type TestStruct struct {
    Name   string `inject`
    Nick   []byte
    Gender SpecialString `inject`
    uid    int           `inject`
    Age    int           `inject`
}

func main() {
    s := TestStruct{}
    inj := inject.New()
    inj.Map("张三")
    inj.MapTo("男", (*SpecialString)(nil))
    inj2 := inject.New()
    inj2.Map(26)
    inj.SetParent(inj2)
    inj.Apply(&s)
    fmt.Println("s.Name =", s.Name)
    fmt.Println("s.Gender =", s.Gender)
    fmt.Println("s.Age =", s.Age)
}
运行结果如下:

s.Name = 张三
s.Gender = 男
s.Age = 26