Golang 手撸 IoC 容器

Golang 2794 字

0x00 什么是 IoC

IoC (控制反转)并不是一个新鲜的概念,如果之前有过 Java 经验,那么肯定不会对 IoC 陌生,而 PHP 近几年的框架其实也开始出现了大量的容器应用,在这里需要注意 IoC 和 IoC 容器是两个概念,不要混淆

IoC 是编程思想,而 IoC 容器是帮助我们进行 IoC 的工具,我们通过控制反转把对象之间的调用过程交给 IOC 容器来完成,而最终实现业务代码的解耦。

在 Golang 中 IOC 容器的使用时候,我们要根据我们的具体情况还进行分析是否要使用 IOC

  • 如果你的项目对于性能要求比较高,最好就不要使用 IOC 容器了,因为在 Golang 中实现 IoC 要使用到反射,而反射使用的 空间,如果大量使用反射就会对 Golang 的 GC 造成一定的压力。
  • 要是你的项目对于性能要求不高,追求的是业务的封装性和可维护性,那么就可以考虑使用 IoC 容器。

如果你不了解什么是 IoC 也没有关系,下面会带大家简单的了解一下什么是 IOC 容器。

下面来简单解释一下什么是 IoC。

img

假设现在有两个 Service:UserService 和 OrderService。

  • UserService: 用户服务, GetUserInfo 获取用户信息
  • OrderService: 订单服务, GetOrderInfo 获取订单信息。

这两个 Service 中的方法传递的参数也都是用户 id,现在以代码的形式来模拟一下这个场景。

初始目录如下:

.
├── go.mod
├── main.go
└── svc           #service 目录
    ├── OrderService.go
    └── UserService.go

UserService代码如下:

package svc

import "fmt"

type UserService struct {
}

func NewUserService() *UserService {
    return &UserService{}
}

func (this *UserService) GetUserInfo(uid int) {
    fmt.Println("获取用户 id = ", uid, "的详细信息")
}

OrderService

package svc

import "fmt"

type OrderService struct {
}

func NewOrderService() *OrderService {
    return &OrderService{}
}

func (this *OrderService) GetOrderInfo(uid int) {
    fmt.Println("获取用户 id = ", uid, "的订单信息")
}

基础的 Service 已经封装好了,接下来进行调用。

package main

import "study-go/svc"

func main() {
    uid := 123
    
    orderSvc := svc.NewOrderService()
    userSvc := svc.NewUserService()

    //获取用户详细
    userSvc.GetUserInfo(uid)
    orderSvc.GetOrderInfo(uid)
}

如果代码写到这个程度是否就可以结束了,答案是可以的,并不是很 Low,在 Golang 中并不是要写成和 Java 一样,有大量的设计模式和类与类之间的嵌套。

如果这个时候时候你的领导跟你说要将获取订单信息的方法放到 UserService 里面,你应该怎么做?

package svc

import "fmt"

type UserService struct {
}

func NewUserService() *UserService {
    return &UserService{}
}

func (this *UserService) GetUserInfo(uid int) {
    fmt.Println("获取用户 id = ", uid, "的详细信息")
}

func (this *UserService) GetOrderInfo(uid int) {
    NewOrderService().GetOrderInfo(uid)
}

你完全可以这样写,因为获取用户订单信息是和用户相关的,没有必要放在两个 Service 里面,这个时候我们可以视作 UserService 依赖于 OrderService ,而我们处理的方式是主动处理依赖(在方法内初始化了 NewOrderService),也就是将依赖硬编码到我们的代码中,也叫做控制正转

调用的时候就变成了这个样子:

package main

import "study-go/svc"

func main() {
    uid := 123
    userSvc := svc.NewUserService()

    //获取用户详细
    userSvc.GetUserInfo(uid)
    userSvc.GetOrderInfo(uid)
}

而我们要讲的是依赖反转是被动的,也就是说在我们的 Service 代码里面不会有任何主动初始化 OrderService 的代码,我们需要将我们代码更改一下:

package svc

import "fmt"

type UserService struct {
    orderSvc *OrderService
}

func NewUserService(order *OrderService) *UserService {
    return &UserService{orderSvc: order}
}

func (this *UserService) GetUserInfo(uid int) {
    fmt.Println("获取用户 id = ", uid, "的详细信息")
}

func (this *UserService) GetOrderInfo(uid int) {
    this.orderSvc.GetOrderInfo(uid)
}

观察上方代码的变化:

  1. 我们首先在 UserService 结构体定时以的时候定义了一个新的属性 orderSvc;
  2. 改造了构造方法,在初始化 UserService 的时候,需要给 UserService 传递一个 OrderService 进来;
  3. 在 GetOrderInfo 方法中,因为在构造 UserService 的时候已经传递来了一个 OrderService ,所以可以通过属性的方法进行调用 orderSvc.GetOrderInfo;

那么在调用的时候就变成了这个样子:

package main

import "study-go/svc"

func main() {
    uid := 123
    
    userSvc := svc.NewUserService(svc.NewOrderService())

    //获取用户详细
    userSvc.GetUserInfo(uid)
    userSvc.GetOrderInfo(uid)
}

这个时候,UserService 就被动的 接收了依赖,这就叫做控制反转,也就是 IOC。

这个时候其实是有一些问题的,如果 UserService 有十七八个依赖,我们要怎么样进行处理?这个时候就需要一个工具来帮助我们统一进行管理那就是 IOC 容器,这一部分内容将在下一小节进行讲解。

0x01 利用 Map 存储 Service

在上一小节讲过 IoC 容器是用来帮助我们处理 IoC 的工具,所以我们需要保存两个 Service 之间的依赖关系。

在编写容器代码前,我们现将 OrderService 改造一下,在其中加上 version。

type OrderService struct {
    version string
}

func NewOrderService() *OrderService {
    return &OrderService{version: "1"}
}

我们通过 version 来判断后面我们放到容器中的 OrderService 和取出来的是否一致。

我们先来实现一个基础版本的容器,使用 Map 来存储依赖关系:

package injector

import "reflect"

type BeanMapper map[reflect.Type]reflect.Value

func (this BeanMapper) add(bean interface{}) {
    t := reflect.TypeOf(bean)
    if t.Kind() != reflect.Ptr {
        panic("require ptr object")
    }
    this[t] = reflect.ValueOf(bean)
}

在 Map 中 key 是 reflect.Type, 而 value 是 reflect.Value。

我们来看 add 方法,其实就是将依赖关系存储到 map 中,在使用的时候再从 map 中取出来,在这里为了简化操作,我们目前值允许指针被加入到 map 中。

package injector

var BeanFactory *BeanFactoryImpl

func init() {
    BeanFactory = NewBeanFactory()
}

type BeanFactoryImpl struct {
    BeanMapper BeanMapper
}

func (this *BeanFactoryImpl) Set(vList ...interface{}) {
    if vList == nil || len(vList) == 0 {
        return
    }
    for _, v := range vList {
        this.BeanMapper.add(v)
    }
}

func NewBeanFactory() *BeanFactoryImpl {
    return &BeanFactoryImpl{BeanMapper: make(BeanMapper)}
}

现在创建和添加已经写好了,在这里,我们使用工厂模式来创建容器,init 函数是初始化函数,他在 main 函数执行前执行,用来初始化资源或者提前创建对象。

现在我们再给容器添加获取代码,首先给 mapper 添加 get 方法,很简单,其实就是依靠 map 的获取操作。

func (this BeanMapper) get(bean interface{}) reflect.Value {
    t := reflect.TypeOf(bean)

    if v, ok := this[t]; ok {
        return v
    }
    return reflect.Value{}
}

然后再添加 Factory 的获取方法。

func (this *BeanFactoryImpl) Get(v interface{}) interface{} {
    if v == nil {
        return nil
    }
    bean := this.BeanMapper.get(v)
    if bean.IsValid() {
        return bean.Interface()
    }
    return nil
}

这样一来我们就完成了容器的添加依赖和获取依赖,我们来实践一下。

package main

import (
    "fmt"
    "study-go/injector"
    "study-go/svc"
)

func main() {
    injector.BeanFactory.Set(svc.NewOrderService())
    orderSvc := injector.BeanFactory.Get((*svc.OrderService)(nil))
    fmt.Println(orderSvc)
}

执行一下:

&{2}

输出的时候将 OrderService 中的 version 给打印了出来,这证明我们可以从容器中取到放进去的值。

0x02 处理依赖注入

我们的 UserService 依赖了 OrderService,我们之前使用手动编码来处理依赖关系,现在我们有了容器,现在我们来使用容器来自动注入。

做法其实有很多方案,在Java 可以使用注解, PHP 中使用注释来模拟注解,而在 Golang 中的处理方式稍有不同,因为在 Golang 中并没有对注解的原生支持,所以我们需要使用 Tag 来实现,Tag 是用于标识结构体字段的额外信息,标准库 reflect 包提供了操作 Tag 的方法[1]

package svc

import "fmt"

type UserService struct {
    OrderSvc *OrderService `inject:"-"`
}

func NewUserService(order *OrderService) *UserService {
    return &UserService{OrderSvc: order}
}

func (this *UserService) GetUserInfo(uid int) {
    fmt.Println("获取用户 id = ", uid, "的详细信息")
}

func (this *UserService) GetOrderInfo(uid int) {
    this.OrderSvc.GetOrderInfo(uid)
}

首先我们将 Uservice 中的 orderSvc 改为公开的属性,然后在其后面增加 inject tag,其中 "-" 其实写不写都无所谓,但是因为我们在业务开发的时候,可能还会有 json tag ,所以我们在这里约定 inject:"-" 就等于我要使用依赖注入。

接下来,我们在 BeanFactory 中实现 Apply 方法,用来处理依赖注入。

func (this *BeanFactoryImpl) Apply(bean interface{}) {

    if bean == nil {
        return
    }

    v := reflect.ValueOf(bean)

    if v.Kind() == reflect.Ptr {
        v = v.Elem()
    }

    if v.Kind() != reflect.Struct {
        return
    }

    for i := 0; i < v.NumField(); i++ {
        field := v.Type().Field(i)
        if v.Field(i).CanSet() && field.Tag.Get("inject") != "" {
            if getV := this.Get(field.Type); getV != nil {
                v.Field(i).Set(reflect.ValueOf(getV))
            }
        }
    }
}

首先在代码中判断了是否为结构体,并且从 map 中获取已经实例化好的 Service,在这里我们还需要改造一下 Map 的 get 方法,因为这个时候传递的 field.Type 在 get 方法中会报错,我们需要兼容一下这个类型,在原有代码中增加一下断言。

func (this BeanMapper) get(bean interface{}) reflect.Value {
    var t reflect.Type
    //断言一下 bean
    if bt, ok := bean.(reflect.Type); ok {
        t = bt
    } else {
        t = reflect.TypeOf(bean)
    }

    if v, ok := this[t]; ok {
        return v
    }
    return reflect.Value{}
}

现在让我们来试一下新的成果。

func main() {
    injector.BeanFactory.Set(svc.NewOrderService())

    userService := svc.NewUserService()
    injector.BeanFactory.Apply(userService)
    
    fmt.Println(userService.OrderSvc)
}

现在我们的依赖管理采用 Apply 方法进行管理,Apply 方法自动在容器中寻找其依赖项,然后注入到 UserService 中,运行程序显示结果如下:

&{2}

0x03 动态注入 & Tag 支持表达式

在这个阶段,我们要是实现一个能够基于 Tag 表达式能够动态注入的功能,因为如果项目不复杂,只有几个 Service,那么我们还可以进行手工的将 Service 添加到容器中,那么如果现在随着业务的增加,Service 的数量增加到 20 个,甚至 100 个,我们就需要在 BeanFactory.Set() 方法的参数中写大量的代码来完成容器的初始化。

这么做很显然是不够好的,以为一个完整的容器是需要能够动态注入的,我们只需要基于简单的配置,就可以完成动态注入。

首先我们先创建一个新的文件 ServiceConfig.go 我们将在 ServiceConfig Struct 中来管理我们依赖的创建。

package config

import "study-go/svc"

type ServiceConfig struct {
}

func NewServiceConfig() *ServiceConfig {
    return &ServiceConfig{}
}

func (this *ServiceConfig) OrderService() *svc.OrderService {
    return svc.NewOrderService()
}

以后业务中所有依赖实例化我们都存放在这个文件中,这种做法其实是类似于 Java Spring 的做法。

上面刚才说了,我们要实现一个能够基于 Tag 动态注入的功能,目前字段 Tag 中给的约定是 "-" ,用来直接去到容器中被实例化好的 Service,我们可以更近一步,将约定在增加一份,也就是所谓的表达式,例如 inject:"ServiceConfig.OrderService()

根据这个表达式可以清晰的看出,该字段依赖于 ServiceConfig.OrderService,那么只需要 BeanFactory 代替我们去把这个值拿到就可以了。

但是目前 BeanFactory 目前只有一个 BeanMap 来存储被实例化好的 struct ,并不支持表达式,所以我们需要先增加一个字段。

type BeanFactoryImpl struct {
    BeanMapper BeanMapper             // 存储依赖
    ExprMap    map[string]interface{} // 新增的字段,利用表达式的方式存储依赖 
}

新增的 ExprMap 中 key 存储的就是我们需要支持的表达式,而他的 value 其实就是我们的实例化好的 struct。

接下来,我们先引入一个新的库 github.com/shenyisyn/goft-expr ,这个库的作用是使用表达式来执行函数或者 struct,我们来修改 BeanFactory 中 的 Apply 方法,以支持新的表达式。

func (this *BeanFactoryImpl) Apply(bean interface{}) {
    if bean == nil {
        return
    }

    v := reflect.ValueOf(bean)

    if v.Kind() == reflect.Ptr {
        v = v.Elem()
    }

    if v.Kind() != reflect.Struct {
        return
    }

    for i := 0; i < v.NumField(); i++ {
        field := v.Type().Field(i)
        if v.Field(i).CanSet() && field.Tag.Get("inject") != "" {

            // 兼容老的方式 注入
            if field.Tag.Get("inject") == "-" {
                if getV := this.Get(field.Type); getV != nil {
                    v.Field(i).Set(reflect.ValueOf(getV))
                }
            } else {
                fmt.Println("使用了表达式")
                ret := expr.BeanExpr(field.Tag.Get("inject"), this.ExprMap)
                if ret != nil && !ret.IsEmpty() {
                    v.Field(i).Set(reflect.ValueOf(ret[0]))
                }
            }

        }
    }
}

我们来看这一段代码:

// 兼容老的方式 注入
if field.Tag.Get("inject") == "-" {
    ...
} else {
        ret := expr.BeanExpr(field.Tag.Get("inject"), this.ExprMap)
        if ret != nil && !ret.IsEmpty() {
        v.Field(i).Set(reflect.ValueOf(ret[0]))
}

首先是判断了 inject 是否等于 - 符号,这样是为了兼容老的表达式,也就是默认去读取依赖的 Service,然后在下方,我们去根据表达式取出了已经在容器中准备好的 Service。

这一切都已经准备好了,我们来先试验一下根据表达式来进行注入,先修改 UserService:

type UserService struct {
    OrderSvc *OrderService `inject:"ServiceConfig.OrderService()"`
}

然后是 main.go

func main() {
  srvConfig := config.NewServiceConfig()
  
    injector.BeanFactory.ExprMap = map[string]interface{}{
        "ServiceConfig": srvConfig,
    }
  
    injector.BeanFactory.Set(srvConfig)

    userService := svc.NewUserService()
    injector.BeanFactory.Apply(userService)

    fmt.Println(userService.OrderSvc)
}

接下来执行结果如下:

➜  study-go go run main.go 
&{2}

接下来,我们继续优化我们的代码,能够让代码真正的动态注入,在上面,我们需要手动配置 ExprMap ,但是其实我们同样可以使用反射,来自动完成这一配置,这一次我们先来看 main.go

func main() {
  srvConfig := config.NewServiceConfig()
  
    injector.BeanFactory.Set(srvConfig)
  
    userService := svc.NewUserService()
    injector.BeanFactory.Apply(userService)

    fmt.Println(userService.OrderSvc)
}

BeanFactory 新增 Config 方法:

func (this *BeanFactoryImpl) Config(cfgs ...interface{}) {
    for _, cfg := range cfgs {
        t := reflect.TypeOf(cfg)
        if t.Kind() != reflect.Pointer {
            panic("required ptr object!")
        }
        this.Set(cfg) //把 cfg 本身也加入到 bean 中
        t = t.Elem()
        this.ExprMap[t.Name()] = cfg  // 自动设置 ExprMap 
        v := reflect.ValueOf(cfg).Elem()
        for i := 0; i < t.NumMethod(); i++ {
            method := v.Method(i)
            callRet := method.Call(nil)
            if callRet != nil && len(callRet) == 1 {
                this.Set(callRet[0].Interface())
            }
        }
    }
}

这段代码其实很好理解:

  1. 判断是否为指针,因为这里我们需要的是被实例化好的 Strcut
  2. 然后将 ServiceConfig 也放入到 BeanMap 当中
  3. 然后设置 ExprMap
  4. 最后序列化 ServiceConfig 中的所有 NewXXX 函数

这样一来动态注入就已经完成了

0x04 支持接口注入

如果要让我们的容器支持接口,其实也非常简单,只需要修改 BeanMap 中的 Get 方法即可:

func (this BeanMapper) get(bean interface{}) reflect.Value {
  ...
    if v, ok := this[t]; ok {
        return v
    }
    //如果没有找到那就去看看接口
    for k, v := range this {
        if k.Implements(t) {
            return v
        }
    }
    return reflect.Value{}
}

0x05 处理循环依赖

当我们的 Service 存在多级嵌套的时候,按照现在的代码逻辑是无法去自动完成下一级的自动注入的,我们仅需要在apply 完成的时候,在对其依赖进行递归调用就可以

// 兼容老的方式 注入
if field.Tag.Get("inject") == "-" {
    if getV := this.Get(field.Type); getV != nil {
          v.Field(i).Set(reflect.ValueOf(getV))
          this.Apply(getV)
    }
} else {
        ret := expr.BeanExpr(field.Tag.Get("inject"), this.ExprMap)
        if ret != nil && !ret.IsEmpty() {
             retV := ret[0]
             v.Field(i).Set(reflect.ValueOf(retV))
             this.Apply(retV)
        }
}

0x06 IOC 容器还需要做的事情

到目前为止,我们的 基础版的 IOC 就已经完成了,但是目前还有两个问题没有解决:

  1. 单例和多例,目前这个 IOC 容器,每一次在执行的时候其实都是会调用 NewXXX 实例化出来一个依赖,所以需要能够支持单例或者多例
  2. ServiceConfig 目前还不支持参数实例化

后面如果有时间的话,我会将这块补上。

0x07 结尾

在这里首先要感谢程序员在囧途的沈逸的课程,这篇文章其实就是以 Golang 手撸 IOC 容器为基础编写的,大部分都是他讲课的内容,我在其基础上进行了一些编辑,并且引用了其他的一些文章内容来充实整篇文章,目前与课程不同的是没有去实现单例和多例。

Ref

maksim
Maksim(一笑,吡罗),PHPer,Goper
OωO
开启隐私评论,您的评论仅作者和评论双方可见