Golang基于泛型实现的简单依赖注入(DI)

发布时间:2022年05月10日 // 分类:代码 // 暂无评论

前言

众所周知,在SpringBoot中@Autowire依赖注入是极其方便的。

在Golang中,也可以利用reflect反射实现简单的依赖注入。但在泛型出现之前,这样的依赖注入在IDE提示上不够优雅,如果实例和interface的类型不对应,需要在运行时才会报错。(可以参考我之前的文章《Golang实现简单的IoC》)

Golang 1.18 终于推出泛型后,我们以利用泛型的特性,在初始化实例容器时得以利用上IDE的类型检查,相对更优雅地降低出错的概率。

代码与例子

完整实现代码和例子,我都放到了Github上,可以直接克隆到本地运行\example\cmd\main.go查看效果。
https://github.com/kentzhu/simpledi

实现的效果

让我们先看看实现的效果。在MVC结构的项目中,我们时常需要对Controller注入依赖服务。利用依赖注入,我们可以简化成这样:

type DemoController struct {
    HelloSvc         service.IHelloService   `inject:""`
    MessageEmptySvc  service.IMessageService `inject:""`
    MessageBananaSvc service.IMessageService `inject:"Banana"`
}

func (d DemoController) Visit() {
    fmt.Printf("Hello: [%s] \n", d.HelloSvc.SayHello())
    fmt.Printf("Message from Empty: [%s] \n", d.MessageEmptySvc.Message())
    fmt.Printf("Message from Banana: [%s] \n", d.MessageBananaSvc.Message())
}

只要加上inject的Tag,就可以根据字段类型进行注入。同时,在相同类型的情况下,我们还可以通过别名注入不同的实例。

有了上面的机制,我们只需要在程序恰当的地方(如程序启动时),做这样两件事:

  1. 创建一个DI容器,并将各类Service存入容器。
  2. 在恰当的地方,利用DI对控制器进行注入。
// 创建一个DI容器
container := simpledi.NewContainer()

// 将被依赖的实例放入DI容器
simpledi.Put[service.IHelloService](container, impl.NewHelloService())
simpledi.Put[service.IMessageService](container, impl.NewMessageService("My name is empty"))
simpledi.PutWithName[service.IMessageService](container, impl.NewMessageService("My name is Banana"), "Banana")

// 使用依赖注入的简单演示
// 注入目标可以是MVC结构Web应用中的控制器
ctl := &controller.DemoController{}
simpledi.Inject(container, ctl)
ctl.Visit()

在上面的例子里,我们可以看到,利用Golang泛型的特性和IDE代码检查提示,我们可以保证放入容器的实例和声明的接口是一致的。

如何实现

实现这个DI也是非常简单的。

  1. 创建一个可以存放实例的容器。
  2. 利用reflect实现对注入目标的扫描和依赖注入。

实例容器

package simpledi

import (
    "fmt"
    "reflect"
)

type IContainer interface {
    Get(key, instanceName string) reflect.Value
    Put(key, instanceName string, instance reflect.Value)
}

func NewContainer() IContainer {
    return &container{
        m: map[string]map[string]reflect.Value{},
    }
}

type container struct {
    m map[string]map[string]reflect.Value
}

func (c *container) Get(key, instanceName string) reflect.Value {
    if _, ok := c.m[key]; !ok {
        panic(fmt.Errorf("simpledi: instance [%s] has not registered", key))
    }
    if _, ok := c.m[key][instanceName]; !ok {
        panic(fmt.Errorf("simpledi: instance [%s] name [%s] has not registered", key, instanceName))
    }
    return c.m[key][instanceName]
}

func (c *container) Put(key, instanceName string, instance reflect.Value) {
    if _, ok := c.m[key]; !ok {
        c.m[key] = map[string]reflect.Value{}
    }
    c.m[key][instanceName] = instance
}

实现注入、实现功能函数

package simpledi

import (
    "fmt"
    "reflect"
)

func Put[T any](c IContainer, instance T) {
    PutWithName[T](c, instance, "")
}

func PutWithName[T any](c IContainer, instance T, instanceName string) {
    interfaceType := reflect.TypeOf((*T)(nil)).Elem()
    c.Put(getInterfaceKey(interfaceType), instanceName, reflect.ValueOf(instance))
}

func Get[T any](c IContainer) T {
    return GetWithName[T](c, "")
}

func GetWithName[T any](c IContainer, instanceName string) T {
    key := getInterfaceKey(reflect.TypeOf((*T)(nil)).Elem())
    return c.Get(key, instanceName).Interface().(T)
}

func Inject(c IContainer, instancePtrToInject any) {
    targetType := reflect.TypeOf(instancePtrToInject)
    if targetType.Kind() != reflect.Ptr {
        panic(fmt.Errorf("simpledi: inject target <%s> is not ptr", targetType.String()))
    }
    targetElemType := targetType.Elem()
    targetElemValue := reflect.ValueOf(instancePtrToInject).Elem()
    targetFieldCount := targetElemType.NumField()
    for i := 0; i < targetFieldCount; i++ {
        fieldType := targetElemType.Field(i)
        instanceName, ok := fieldType.Tag.Lookup("inject")
        if !ok {
            continue
        }
        interfaceKey := getInterfaceKey(fieldType.Type)
        targetElemValue.FieldByIndex(fieldType.Index).Set(c.Get(interfaceKey, instanceName))
    }
}

func getInterfaceKey(interfaceType reflect.Type) string {
    return fmt.Sprintf("%s.%s", interfaceType.PkgPath(), interfaceType.Name())
}

拓展

  1. 这个简单的实现并未做并发保护。如果出现多线程同时操作容器的情况,可能会出现问题。可以考虑在Container中使用读写锁保护。当然,初始化DI容器一般在服务器初始化过程中就完成了,一般情况下是不会有并发写入的情况。
  2. 在SpringBoot中,注入的Bean可以是单例模式、原型模式、工厂模式等。而在我们这个简单的DI实现中,只支持单例的情况。如果要支持原型模式、工厂模式,则需要对Get和Put进行改造。这里暂时就不实现了。

本文固定链接
https://www.ywlib.com/archives/a-golang-simple-di-with-generics.html

标签
golang, ioc, di, generics, 泛型, 依赖注入

添加新评论 »