Golang 依赖注入库 goioc/di 用法和原理解析

2,729 阅读10分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第17天,点击查看活动详情

基于 Golang 的依赖注入框架有很多,google/wire, facebook/inject, uber/dig 等等。其实和 ORM 类似,依赖注入底层也需要大量的反射进行支持。笔者近期在工作中使用了 goioc/di 这个框架,体验还不错,如果你是一个从 Java 转 Golang 的开发者,详细看到 goioc/di 的接口时会有一种亲切感。

今天我们一起看看它如何应用,以及实现的原理。

在开始阅读之前,如果有余力我建议大家还是好好看看 Martin Fowler 大神的这一篇 blog,它会告诉你为什么我们需要去注入依赖,为什么要 Inversion of Control:www.martinfowler.com/articles/in…

goioc/di 上手

Simple and yet powerful Dependency Injection for Go

仓库:github.com/goioc/di

di 的用法很简单,我们参考官方示例走一遍看看 (如果怕麻烦,也可以直接看源码

我们想要实现的是一个非常简单的 http 服务,它接受一个 path 参数 city(可以是任何你想查询天气的城市),然后调用 wttr.in/ 拿到城市的天气情况。

  1. 首先参照下图结构创建目录,

image.png

  • controllers :顾名思义,一个具体的 API handler,这里不会做业务逻辑,但是会处理好 http 请求和响应与业务的输入输出之间的结构转换;

  • services : 具体的业务逻辑;

  • init.go : 依赖 goioc/di 进行注入,初始化 container;

  • main.go : 启动server,监听端口。

  1. 补充业务实现
  • services/weather_service.go
package services

import (
	"io/ioutil"
	"net/http"
)

type WeatherService struct {
}

func (ws *WeatherService) Weather(city string) (*string, error) {
	response, err := http.Get("https://wttr.in/" + city)
	if err != nil {
		return nil, err
	}
	all, err := ioutil.ReadAll(response.Body)
	if err != nil {
		return nil, err
	}
	weather := string(all)
	return &weather, nil
}

可以看到,WeatherService 本身没有任何依赖,提供的 Weather 是个纯业务逻辑方法,接受一个入参,输出结果和 error。

类似 WeatherService 这样没有其他需要注入的依赖的结构,其实才是 inject 体系里面最底层的依赖,先有了这些 bean,才可能去初始化其他对 WeatherService 有依赖的 bean。

  • controllers/weather_controller.go
package controllers

import (
	"net/http"

	"github.com/ag9920/di-demo/services"
)

type WeatherController struct {
	// note that injection works even with unexported fields
	weatherService *services.WeatherService `di.inject:"weatherService"`
}

func (wc *WeatherController) Weather(w http.ResponseWriter, r *http.Request) {
	weather, _ := wc.weatherService.Weather(r.URL.Query().Get("city"))
	_, _ = w.Write([]byte(*weather))
}

这里也很好理解,作为 controller,WeatherController 的职责在于转发,参数转换。所以他是需要依赖 services.WeatherService 的。这里也是很常见的用 tag 的方式 di.inject:weatherService" 来声明此处需要注入的 bean 的名称。

  • init.go
package main

import (
	"reflect"

	"github.com/ag9920/di-demo/controllers"
	"github.com/ag9920/di-demo/services"

	"github.com/goioc/di"
)

func init() {
	_, _ = di.RegisterBean("weatherService", reflect.TypeOf((*services.WeatherService)(nil)))
	_, _ = di.RegisterBean("weatherController", reflect.TypeOf((*controllers.WeatherController)(nil)))
	_ = di.InitializeContainer()
}

这里是完成整体依赖注入初始化的地方,声明了 weatherService 以及 weatherController 两个bean,以及对应的类型,注册到 container 里面,最后 InitializeContainer() 完成初始化。

  • main.go
package main

import (
	"net/http"

	"github.com/ag9920/di-demo/controllers"

	"github.com/goioc/di"
)

func main() {
	http.HandleFunc("/weather", func(w http.ResponseWriter, r *http.Request) {
		di.GetInstance("weatherController").(*controllers.WeatherController).Weather(w, r)
	})
	_ = http.ListenAndServe(":8080", nil)
}

main 函数启动了 http server,这里可以看到,要获取 controller 实例,是通过 di.GetInstance 实现的,拿到了 interface{},再进行类型转换。

好,代码看完了,我们总结一下目前为止看到的 di 用法:

  1. 通过 di.inject:"{{ name of bean }}" 来声明需要注入的 bean;
  2. 初始化时,通过 di.RegisterBean 将各个 bean 注册进来。在所有注册完成后,调用 di.InitializeContainer() 进行注入;
  3. 当需要获取注入的 bean 时,可以采用di.GetInstance("{{ name of bean }}") 来拿到一个 interface{},再转成实际的类型。

单例 vs 原型

这个部分我们结合源码和 API 文档来看看,依赖注入是怎么做到的。

在依赖注入中通常存在两种模式:

  • 单例:直接注入单例对象,只存在一个实例,无论什么时候通过 name 来获取 bean 都会返回这一个;
  • 原型:注入一个类型,每次需要的时候新创建一个实例。

goioc/di 也支持了这两个,分别对应 Singleton 和 Prototype,同时还补充了一个 http request 声明周期的 scope:Request:

// Scope is an enum for bean scopes supported in this IoC container.
type Scope string

const (
	// Singleton is a scope of bean that exists only in one copy in the container and is created at the init-time.
	// If the bean is singleton and implements Close() method, then this method will be called on Close (consumer responsibility to call Close)
	Singleton Scope = "singleton"
	// Prototype is a scope of bean that can exist in multiple copies in the container and is created on demand.
	Prototype Scope = "prototype"
	// Request is a scope of bean whose lifecycle is bound to the web request (or more precisely - to the corresponding
	// context). If the bean implements Close() method, then this method will be called upon corresponding context's
	// cancellation.
	Request Scope = "request"
)

同样的,除了我们上一节看到的 RegisterBean 这种给出 type 即可注册的形式,goioc/di 也提供了直接注册单例的 RegisterBeanInstance 方法,此后所有对同名 bean 存在依赖的 bean,都会用到同样的单例。

RegisterBean 由于只是提供了类型,理论上讲支持两种 scope,默认为 Singleton,也可以通过

type SomeBean struct {
  Scope Scope `di.scope:"prototype"`
}

这样的注解来声明当前字段为 Protoype 的模式。

而 RegisterBeanInstance 则只提供了 Singleton 这一种模式,作者也提到,如果再去 clone 提供的 instance 来实现 Prototype 的话复杂度会提高,从使用的角度来说没有强诉求,暂时没有提供。

源码解析

这一节我们来看源码,了解一下是怎么实现的。

其实 goioc/di 的原理很简单,是很好的 Golang 反射能力训练的样本。可以想见,对于结构体中哪些成员要注入,注入的模式,都是通过 tag 实现,这就势必要用到 reflect 包的强大功能。类似的,如果是 Prototype 的模式,我们需要基于一个类型来创建一个对象,并进行依赖注入,势必也会频繁反射 reflect.Type。

goioc/di 声明了几个全局的变量,这是依赖注入最终的产物,了解了这些,其实原理就清晰了:

var beans = make(map[string]reflect.Type)
var scopes = make(map[string]Scope)
var singletonInstances = make(map[string]interface{})
var userCreatedInstances = make(map[string]bool)

一句话:di 实现注入的本质就是将每一次 Register 的类型/实例解析好之后,把相关的信息放到这些全局变量中。最后基于这些变量对外提供 GetInstance 接口。

  • beans: 所有 bean 类型的集合, key:bean 的名称,value:注入的类型;

  • scopes: 所有 bean 配置的 scope 集合, key: bean 的名称,value: 设置的 Scope (枚举,上面提到的三种)

  • singletonInstances: 单例beans的集合,同样是名称映射到实例,只不过存的是 interface{};

  • userCreatedInstances: 标记哪些是用户自己传入的单例,所以 value 是个 bool,这里其实是一个 set 的语义。

RegisterBean

我们先来看注入类型的方法 RegisterBean,随后再看注入单例的 RegisterBeanInstance。注意参考补充的中文注释:

// RegisterBean function registers bean by type, the scope of the bean should be defined in the corresponding struct
// using a tag `di.scope` (`Singleton` is used if no scope is explicitly specified). `beanType` should be a reference
// type, e.g.: `reflect.TypeOf((*services.YourService)(nil))`. Return value of `overwritten` is set to `true` if the
// bean with the same `beanID` has been registered already.
func RegisterBean(beanID string, beanType reflect.Type) (overwritten bool, err error) {

        // 全局一把锁,防止出现并发,毕竟前面可以看到,声明的全是普通的 map
	initializeShutdownLock.Lock()
	defer initializeShutdownLock.Unlock()
        
        // 原子CAS,经典操作,避免被重复 initialize,若已经初始化了,就不能再 Register
	if atomic.CompareAndSwapInt32(&containerInitialized, 1, 1) {
		return false, errors.New("container is already initialized: can't register new bean")
	}
        
        // 注册的也必须是个指针类型
	if beanType.Kind() != reflect.Ptr {
		return false, errors.New("bean type must be a pointer")
	}
        
        // 判断之前是否已经注册过了(这里就出现了我们前面说的全局变量 beans 这个 map)
	var existingBeanType reflect.Type
	var ok bool
	if existingBeanType, ok = beans[beanID]; ok {
		logrus.WithFields(logrus.Fields{
			"id":              beanID,
			"registered bean": existingBeanType,
			"new bean":        beanType,
		}).Warn(beanAlreadyRegistered)
	}
        
        // 根据 beanType 来判断要用哪个 scope
	beanScope, err := getScope(beanType)
	if err != nil {
		return false, err
	}
	beanTypeElement := beanType.Elem()
        
        // 遍历结构体,找到 di.inject tag 的字段,判断能够去 inject,类型是否支持
	for i := 0; i < beanTypeElement.NumField(); i++ {
		field := beanTypeElement.Field(i)
		if _, ok := field.Tag.Lookup(string(inject)); !ok {
			continue
		}
		if field.Type.Kind() != reflect.Ptr && field.Type.Kind() != reflect.Interface &&
			field.Type.Kind() != reflect.Slice && field.Type.Kind() != reflect.Map {
			return false, errors.New(unsupportedDependencyType)
		}
	}
        
        // 校验通过,赋值到全局变量即可(这个地方虽然叫 beanID,语义上是唯一的一个 bean 名称,实际是个 string)
	beans[beanID] = beanType
	scopes[beanID] = *beanScope
	return ok, nil
}

看注释应该比较清晰,这里逻辑并不复杂,除去校验逻辑外,真正实现注册的就是获取对应的 scope,然后赋值给 beans 以及 scopes 这两个 map。

类似的,我们来看一下 RegisterBeanInstance 的实现:

// RegisterBeanInstance function registers bean, provided the pre-created instance of this bean, the scope of such beans
// are always `Singleton`. `beanInstance` can only be a reference or an interface. Return value of `overwritten` is set
// to `true` if the bean with the same `beanID` has been registered already.
func RegisterBeanInstance(beanID string, beanInstance interface{}) (overwritten bool, err error) {
	initializeShutdownLock.Lock()
	defer initializeShutdownLock.Unlock()
	if atomic.CompareAndSwapInt32(&containerInitialized, 1, 1) {
		return false, errors.New("container is already initialized: can't register new bean")
	}
	beanType := reflect.TypeOf(beanInstance)
	if beanType.Kind() != reflect.Ptr {
		return false, errors.New("bean instance must be a pointer")
	}
	var existingBeanType reflect.Type
	var ok bool
	if existingBeanType, ok = beans[beanID]; ok {
		logrus.WithFields(logrus.Fields{
			"id":                beanID,
			"registered bean":   existingBeanType,
			"new bean instance": beanType,
		}).Warn(beanAlreadyRegistered)
	}
	beans[beanID] = beanType
	scopes[beanID] = Singleton
	singletonInstances[beanID] = beanInstance
	userCreatedInstances[beanID] = true
	return ok, nil
}

你会发现,几乎一模一样,仅有的差异在于倒数第二和倒数第三行:

singletonInstances[beanID] = beanInstance
userCreatedInstances[beanID] = true

除了赋值给 beans 以及 scopes。对于注册单例的场景,会额外将 instance 放到 singletonInstances 这个全局 map 中,并且在 userCreatedInstances 中声明一下,这个是用户指定的单例。

InitializeContainer

初始化 container 的函数 InitializeContainer 逻辑同样不复杂,比较诧异的是,看完全局居然没有一个 默认 container 对象的存在,goioc/di 是直接完全依赖几个全局 map 来提供能力。

下面这部分代码参考中文注释看一下:

// InitializeContainer function initializes the IoC container.
func InitializeContainer() error {
        // 跟 Register 同样的一把锁
	initializeShutdownLock.Lock()
	defer initializeShutdownLock.Unlock()
        
        // 同样,如果已经初始化了,这时候不应该继续处理
	if atomic.CompareAndSwapInt32(&containerInitialized, 1, 1) {
		return errors.New("container is already initialized: reinitialization is not supported")
	}
        
        // 针对 beans 中的每一个 bean type,都创建一个对应的 instance(如果已经有了,会复用)
	err := createSingletonInstances()
	if err != nil {
		return err
	}
        
        // 针对每一个创建的 instance 注入依赖
	err = injectSingletonDependencies()
	if err != nil {
		return err
	}
        
        // 原子操作,表明已经初始化完成
	atomic.StoreInt32(&containerInitialized, 1)
        
        // 针对 bean 做一些后续的业务初始化操作
	err = initializeSingletonInstances()
	if err != nil {
		return err
	}
	return nil
}

InitializeContainer 这一层的逻辑是很清晰的,真正起作用的就是 createSingletonInstances 以及 injectSingletonDependencies 两个。前者看看还有哪些 bean 是没有创建实例的,如果已经有了就忽略,如果没有,就利用反射创建出来一个对象:

reflect.New(beans[beanID].Elem()).Interface()

注意,此处 beans[beanID] 本质上就是拿这个 bean 的类型。

核心逻辑在于 injectSingletonDependencies,我们来看看。

func injectSingletonDependencies() error {
        // 遍历所有创建出来的单例,调用 injectDependencies 实现注入
	for beanID, instance := range singletonInstances {
		if _, ok := userCreatedInstances[beanID]; ok {
			continue
		}
		if _, ok := beanFactories[beanID]; ok {
			continue
		}
		err := injectDependencies(beanID, instance, make(map[string]bool))
		if err != nil {
			return err
		}
	}
	return nil
}

为了简化理解,我把switch 里面的具体处理略过了,我们先把握主线。

func injectDependencies(beanID string, instance interface{}, chain map[string]bool) error {
  // 先拿到 bean 的 type
  instanceType := beans[beanID]
  instanceElement := instanceType.Elem()
  
  // 针对每个 bean type,遍历结构体字段,挨个处理注入
  for i := 0; i < instanceElement.NumField(); i++ {
    // 拿到 field
    field := instanceElement.Field(i)
    
    // 如果没 inject 的 tag 就忽略
    beanToInject, ok := field.Tag.Lookup(string(inject))
    if !ok {
      continue
    }
    // 如果 optional tag 声明不是 bool 就报错,不合规范
    optionalDependency, err := isOptional(field)
    if err != nil {
      return err
    }
    
    // 拿到实例,找到对应的 field,利用反射,将要注入的 field 转化为 reflect.Value
    fieldToInject := reflect.ValueOf(instance).Elem().Field(i)
    fieldToInject = reflect.NewAt(fieldToInject.Type(), unsafe.Pointer(fieldToInject.UnsafeAddr())).Elem()
    
    // 根据具体的 field 类型,进行注入,这里省略处理的部分
    // 最后都会体现在 fieldToInject.Set(xxxxx)
    switch fieldToInject.Kind() {
    case reflect.Ptr, reflect.Interface:
      // ...
    case reflect.Slice:
      // ...
    case reflect.Map:
      // ...
    default:
      return errors.New(unsupportedDependencyType)
    }
  }
  return nil
}

可以看到,这里的精华在于,要注入,你首先要拿到那个字段的控制权,从 instance 找到字段,然后转成 reflect.Value,从已有的 instance 里面找到所需要的 bean 之后,通过一个 Set 就能更新进来。

对反射不熟练的同学可以仔细品味一下这里的处理,尤其是用 reflect.NewAt 拿到指针的这一行:

fieldToInject := reflect.ValueOf(instance).Elem().Field(i)

fieldToInject = reflect.NewAt(fieldToInject.Type(), unsafe.Pointer(fieldToInject.UnsafeAddr())).Elem()

fieldToInject.Set(reflect.ValueOf(instanceToInject))

switch 中省略的部分,我们拿出来 Ptr 以及 Interface 这个分支来看一看,这里比较本质,slice 和 map 更多的是兼容逻辑。

switch fieldToInject.Kind() {
  case reflect.Ptr, reflect.Interface:
    if beanToInject == "" { // injecting by type, gotta find the candidate first
      candidates := findInjectionCandidates(fieldToInject.Type())
      if len(candidates) < 1 {
        if optionalDependency {
          continue
        } else {
          return errors.New("no candidates found for the injection")
        }
      }
      if len(candidates) > 1 {
        return errors.New("more then one candidate found for the injection")
      }
      beanToInject = candidates[0]
    }
    beanToInjectType := beans[beanToInject]
    logInjection(beanID, instanceElement, beanToInject, beanToInjectType)
    beanScope, beanFound := scopes[beanToInject]
    if !beanFound {
      if optionalDependency {
        logrus.Trace("no dependency found, injecting nil since the dependency marked as optional")
        continue
      } else {
        return errors.New("no dependency found")
      }
    }
    if beanScope == Request {
      return errors.New(requestScopedBeansCantBeInjected)
    }
    instanceToInject, err := getInstance(context.Background(), beanToInject, chain)
    if err != nil {
      return err
    }
    fieldToInject.Set(reflect.ValueOf(instanceToInject))

我们可以看到,上来先判断 beanToInject 是否为空,这个就是从 inject tag 拿到的值。意味着什么呢?

你可以像我们此前这样,明确想要的 bean 是什么:

type WeatherController struct {
	// note that injection works even with unexported fields
	weatherService *services.WeatherService `di.inject:"weatherService"`
}

其实,也可以这样:

type WeatherController struct {
	// note that injection works even with unexported fields
	weatherService *services.WeatherService `di.inject:""`
}

不显式地提供 bean 的名称,而是让 goioc/di 帮你寻找一个实现。这就是 inject by type。

后面 getInstance 的部分相对简单了,就是从已经生成的 singletonInstances 里寻找合适的 bean 返回即可(下面一节会介绍)。最后调用 Set 写入:

instanceToInject, err := getInstance(context.Background(), beanToInject, chain)
if err != nil {
        return err
}
fieldToInject.Set(reflect.ValueOf(instanceToInject))

GetInstance

好了,上面我们看懂了是怎么 Register,以及怎么初始化完成注入的。其实可以看出来 goioc/di 整体代码还是很清晰的,阅读比较顺畅。最后一步,我们来看看 GetInstance 是怎么实现的。

// GetInstance function returns bean instance by its ID. It may panic, so if receiving the error in return is preferred,
// consider using `GetInstanceSafe`.
func GetInstance(beanID string) interface{} {
	beanInstance, err := GetInstanceSafe(beanID)
	if err != nil {
		panic(err)
	}
	return beanInstance
}

// GetInstanceSafe function returns bean instance by its ID. It doesnt panic upon explicit error, but returns the error
// instead.
func GetInstanceSafe(beanID string) (interface{}, error) {
	if atomic.CompareAndSwapInt32(&containerInitialized, 0, 0) {
		return nil, errors.New("container is not initialized: can't lookup instances of beans yet")
	}
	if scopes[beanID] == Request {
		return nil, errors.New("request-scoped beans can't be retrieved directly from the container: they can only be retrieved from the web-context")
	}
	return getInstance(context.Background(), beanID, make(map[string]bool))
}

鉴于我们目前还不关心 Request 这种 scope,可以看到,除了校验逻辑,本质上底层是直接调用了 getInstance 函数,上面两个都是空壳子。

下面来了,核心部分,参考中文注释理解:


func getInstance(ctx context.Context, beanID string, chain map[string]bool) (interface{}, error) {
        // 必须注册过类型
	if !isBeanRegistered(beanID) {
		return nil, errors.New("bean is not registered: " + beanID)
	}
        // 如果是默认的单例模式,直接就返回了 全局 singletonInstances 里存的对应实例
	if scopes[beanID] == Singleton {
		return singletonInstances[beanID], nil
	}
        
        
        // 从这里开始,都是支持 Prototype 以及 Request 两种scope的
        
        // 由于存在嵌套,一定需要检查是不是循环依赖了
	if _, ok := chain[beanID]; ok {
		return nil, errors.New("circular dependency detected for bean: " + beanID)
	}
	chain[beanID] = true
        
        // 查询时创建实例
	instance, err := createInstance(ctx, beanID)
	if err != nil {
		return nil, err
	}
	if _, ok := beanFactories[beanID]; !ok {
		err := injectDependencies(beanID, instance, chain)
		if err != nil {
			return nil, err
		}
	}
        
        // 这里是执行实例创建后的初始化操作,postConstruct
        // 对应于 singleton 里的初始化操作,是在 InitializeContainer 的时候做的,可以往上翻翻
	err = initializeInstance(beanID, instance)
	if err != nil {
		return nil, err
	}
	err = setContext(ctx, beanID, instance)
	if err != nil {
		return nil, err
	}
	return instance, nil
}

这里有一点比较有意思,有时候我们会希望在一个 bean 可用之后进行一些额外的业务操作,goioc/di 提供的能力在这里:

// InitializingBean is an interface marking beans that need to be additionally initialized after the container is ready.
type InitializingBean interface {
	// PostConstruct method will be called on a bean after the container is initialized.
	PostConstruct() error
}

一个 PostConstruct() 方法,允许业务自己定义在 bean 上面,在上面的 initializeInstance 的时候调用。当然,在 singleton 模式下,由于 bean 本身是在初始化 container 的环节创建的,对应的 initializeInstance 时机也会不同。我们来看下这个函数干了什么:

func initializeInstance(beanID string, instance interface{}) error {
	initializingBean := reflect.TypeOf((*InitializingBean)(nil)).Elem()
	bean := reflect.TypeOf(instance)
	if bean.Implements(initializingBean) {
		initializingMethod, ok := bean.MethodByName(initializingBean.Method(0).Name)
		if !ok {
			return errors.New("unexpected behavior: can't find method PostConstruct() in bean " + bean.String())
		}
		logrus.WithField("beanID", beanID).Trace("initializing bean")
		errorValue := initializingMethod.Func.Call([]reflect.Value{reflect.ValueOf(instance)})[0]
		if !errorValue.IsNil() {
			return errorValue.Elem().Interface().(error)
		}
	}
	if postprocessors, ok := beanPostprocessors[bean]; ok {
		logrus.WithField("beanID", beanID).Trace("postprocessing bean")
		for _, postprocessor := range postprocessors {
			if err := postprocessor(instance); err != nil {
				return err
			}
		}
	}
	return nil
}

非常巧妙,怎样在运行时检验某个 struct 是否实现了 某个 interface 呢?

这里前两行就是示例代码:

initializingBean := reflect.TypeOf((*InitializingBean)(nil)).Elem()
bean := reflect.TypeOf(instance)
if bean.Implements(initializingBean) {}

拿到一个空的接口的 reflect.Type,然后拿到实例的 Type,直接看 Implements 的结果。

除此之外,goioc/di 还提供了 postprocessor,供开发者进一步扩展你想进行的操作,提供形如func(bean interface{}) error 的函数,注册进来即可。

// RegisterBeanPostprocessor function registers postprocessors for beans. Postprocessor is a function that can perform
// some actions on beans after their creation by the container (and self-initialization with PostConstruct).
func RegisterBeanPostprocessor(beanType reflect.Type, postprocessor func(bean interface{}) error) error {
	initializeShutdownLock.Lock()
	defer initializeShutdownLock.Unlock()
	if atomic.CompareAndSwapInt32(&containerInitialized, 1, 1) {
		return errors.New("container is already initialized: can't register bean postprocessor")
	}
	beanPostprocessors[beanType] = append(beanPostprocessors[beanType], postprocessor)
	return nil
}

总体来说,goioc/di 的代码还是非常整洁,阅读性很高的,建议大家有时间的话完整地过一遍源码。 在 container 初始化后进行后续扩展的方面,也提供了 PostContruct 以及 PostProcessor 两层能力。