从 Go 依赖注入聊聊 Macaron 框架的“黑魔法”

前阵子本来打算重构博客,但是后来用 WordPress 的官方 Docker 镜像替代了之前自己配的 PHP 环境,问题就解决了。(猜测可能是之前的 PHP 环境没开 Opcache)现在网站的访问速度飞快,我其实也就没必要再浪费时间造轮子了。

又一个刚开的坑就这样匆匆结束了呀。🤣
重构时我使用的是 Macaron 框架,因为在他上面我看到了很多不可思议的亮点。Macaron 完美的解决了我之前用 Beego 时遇到的种种痛点,这让我很是兴奋。虽然这个站没能写完,但是我体验了 Macaron 里的种种“黑魔法”。遂打算写一篇文章来分享介绍一下。

什么是依赖注入?

首先要隆重介绍的就是依赖注入,这是一个在 Java 开发中经常用到的概念,又称控制反转。它让每一块代码只负责实现自己的功能,其他的交给“其他人”去做,当需要什么变量时,就和这个“其他人”说一声,他就会给你提供
举个更形象的例子,依赖注入使得函数的入参不再是固定的:

func Foo(a string){
    // Do something
}
func Bar(a string, b int){
    // Do something
}

当调用Foo函数时,它只需要一个string类型的参数,那么会从一个容器中取出string类型变量的值传入调用;而当调用Bar函数时,它需要一个string类型的参数和一个int类型的参数,那么就会从容器中取出stringint类型的变量传入函数中。

这其实就是所谓的“控制反转”,被调用者不再受制于调用者的入参约定,而是调用者来设法满足被调用者。

再简单点说:使用依赖注入可以实现调用任意入参的函数,它会自己找数据注入填充。

依赖注入的 Go 语言实现

Macaron 框架的很多中间件的载入都使用到了依赖注入,需要导入go-macaron/inject包。inject包的代码虽然很少,但对于像我这样的菜鸡,实在不太容易理解。😆

我们慢慢来理一下,实现依赖注入,那么肯定需要一个容器,来存储我们需要注入的所有数据。还需要有一个方法(接口)用于实现将数据注入到实体对象中,这个实体对象在 Go 里一般是一个结构体。还需要一个方法(接口)来调用实际的函数。
因此我们就可以得到inject包的Injector接口,这是实现依赖注入的容器需要实现的接口。

type Injector interface {
    Applicator // 这个接口用来灌入到一个结构体
    Invoker    // 这个接口用来实际调用的,所以可以实现非反射的实际调用
    TypeMapper // 这个接口是真正的容器
    SetParent(Injector) // 表示这个结构是递归的
}

TypeMapper

首先是TypeMapper接口:

type TypeMapper interface {
    // 直接设置一个对象,TypeOf 是 key,value 是这个对象
    Map(interface{}) TypeMapper
    // 将一个对象注入到一个接口中,TypeOf 是接口,value 是对象,因为接口直接 TypeOf 获取只能拿到接口类型
    MapTo(interface{}, interface{}) TypeMapper
    // 手动设置 key 和 value
    Set(reflect.Type, reflect.Value) TypeMapper
    // 从容器中获取某个类型的注入对象
    Get(reflect.Type) reflect.Value
}

然后再看下inject包中的实现了该接口的injector对象:

type injector struct {
    values map[reflect.Type]reflect.Value
    parent Injector
}

需要重点注意的是这个结构体里的values,这个 map 就是用于存储注入对象的容器,它的 key 是使用反射获取到的数据的类型,value 是获取到的数据的值。

注意:这里是直接用 map 进行存储和读取,因此是并发不安全的。但是….. 你几乎不可能写出需要并发读取这个 map 的代码。(如果有,那说明你一开始可能就设计错了。

Invoker

之后就是该结构体实现的Invoke函数:

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函数用于根据函数需要的入参,从values容器中取出相应的值传入函数并调用。
可以看到他这里用反射获取了函数所需要的入参数量以及类型,然后调用Get()通过参数类型获得参数的值。最后使用反射调用函数。

注意 Get() 方法,以参数类型为 key 从 `values`这个 map 中拿到参数的值——因此一种类型只会有一个值。

比如当我们的函数入参有两个 string 时,通过依赖注入获取到的 string 的值将会是同一个。这时我们需要自己额外声明一个类型来解决。

func Foo(firstName string, lastName string){
    // Do something
}
type mystring string
func Bar(firstName string, lastName mystring)

👆这样就把两个参数通过类型给区分开来了。

同时,在 Macaron 框架的inject包中,无闻又加了一个FastInvoker方法:

type FastInvoker interface {
    // Invoke attempts to call the ordinary functions. If f is a function
    // with the appropriate signature, f.Invoke([]interface{}) is a Call that calls f.
    // Returns a slice of reflect.Value representing the returned values of the function.
    // Returns an error if the injection fails.
    Invoke([]interface{}) ([]reflect.Value, error)
}

实现这个接口后,相当于原结构体可以自己实现函数的调用方法。inject包原本的Invoke使用反射来调用函数,保证对所有的函数都适配,但FastInvoker接口使得结构体可以自行专门定义一个自己的Invoke方法,使得函数不再需要通过反射调用,进而提升了性能。

Applicator

Applicator 就是将数据值注入进结构体中,当结构体中字段带有inject的 tag 时,便会从values容器中获取值,使用反射给结构体字段赋值。

一个简单的 Demo

type noString string

no := noString("10000")     // 为了区分两个 string,定义了新的类型
name := "E99p1ant"
age := 20

inj := inject.New()
inj.Map(no)
inj.Map(name)
inj.Map(age)
inj.Invoke(func(age int) {
    fmt.Printf("Your age is %d\n", age)
})
inj.Invoke(func(no noString, name string) {
    fmt.Printf("Hi! %s - %s\n", no, name)
})

Context

以上介绍完了依赖注入,我们可以来看下 Macaron 的框架的 Context。

type Context struct {
    inject.Injector
    handlers []Handler
    ...

可以看到Context实现了inject.Injector接口,所以我们可以对其进行依赖注入。同时,Macaron 路由的 Handler 入参是interface{},因此我们可以传入入参为自定义中间件的函数。

type MyContext struct {
    *macaron.Context
    Name string
}

func (c *MyContext) Greet() {
    c.Write([]byte("Hello " + c.Name))
}

func main() {
    m := macaron.Classic()
    m.Use(func(ctx *macaron.Context) {
        c := &MyContext{
            Context: ctx,
            Name:    "",
        }
        ctx.Map(c)
    })
    m.Get("/", func(c *MyContext) {
        c.Greet()
    })
    m.Run()
}

可以看到这里我们声明了一个自己的 Context 上下文,它继承了*macaron.Context,并拥有了Name属性和Greet()方法。我们使用添加中间件的方法,将我们声明的 Context 注入进 Macaron 原本的 Context 中。
*macaron.Contextrun()方法即为调用 Handlers 的地方:

func (c *Context) run() {
    for c.index <= len(c.handlers) {
        vals, err := c.Invoke(c.handler())
        if err != nil {
            panic(err)
        }
        c.index += 1
        ...

这里使用了*macaron.ContextInjector接口中的Invoke来调用 Handler,Handler 函数的*MyContext入参为我们之前注入进容器的数据。
有了这个特性,我们可以将 cache、i18n、session 等功能集成进自定义的 Context 中,还可以将常用的属性(比如isLogin),作为结构体字段加入到 Context 中,大大方便了我们的调用。

具体的实践可以参考 gogs

binding

另一个使用依赖注入实现的“黑魔法”是 binding 包,他是用来验证和绑定传入的表单的。
因为有了依赖注入,我们可以直接将绑定表单的结构体作为参数传进 Handler 中:

func CategoryPost(c *context.Context, f form.NewCategory) {
    if !c.HasError() {
        err := db.Categories.New(&db.Category{
            Name: f.Name,
        })
        if err != nil {
            c.RenderWithErr(err.Error(), "admin/category/category", f)
            return
        }
    }
    c.Data["CategoryList"] = db.Categories.All()
    c.Success("admin/category/category")
}

是不是很神奇!以往需要从 Context 中取的变量,现在可以单独作为函数的参数!
而在声明路由的时候:

r.Post("/category", binding.Bind(form.NewCategory{}), admin.CategoryPost)

我们使用了binding.Bind方法来指定需要绑定的表单结构体。简单地跟一下代码:
1. 请求的 body 被传入 bind 方法中(binding.go L104)
2. 这里以传入 JSON 为例,进入 Json 方法解析 body(binding.go L45)
3. 方法的最后validateAndMap方法(binding.go L210)

func validateAndMap(obj reflect.Value, ctx *macaron.Context, errors Errors, ifacePtr ...interface{}) {
    _, _ = ctx.Invoke(Validate(obj.Interface()))
    errors = append(errors, getErrors(ctx)...)
    ctx.Map(errors)
    ctx.Map(obj.Elem().Interface())
    if len(ifacePtr) > 0 {
        ctx.MapTo(obj.Elem().Interface(), ifacePtr[0])
    }
}

可以看到在这里,绑定好数据的新结构体被注入进 Context 中。
之后再从 Context 中取得。

总结

以上就是我最近用到的 Macaron 使用依赖注入实现的“黑科技”。感觉 Macaron 才是一个 Go Web 框架该有的样子。虽然他在一定程度上使用了过多的反射,但却提高了开发的效率。
文章中如有描述不准确的地方,还请小伙伴们指出。



喜欢这篇文章?为什么不打赏一下呢?

1 条评论

昵称
  1. fumeboy

    控制反转可以这样做: func Bar(a string, b int) ->>

    type params struct{ a string, b int }
    func (p* params) bar()

    可以参考我的这个项目 https://github.com/fumeboy/nji