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

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

什么是依赖注入?

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

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

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

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

依赖注入的 Go 语言实现

inject
injectInjector
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
}
injectinjector
type injector struct {
    values map[reflect.Type]reflect.Value
    parent Injector
}
values
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
}
invokevaluesGet()

比如当我们的函数入参有两个 string 时,通过依赖注入获取到的 string 的值将会是同一个。这时我们需要自己额外声明一个类型来解决。
func Foo(firstName string, lastName string){
    // Do something
}
type mystring string
func Bar(firstName string, lastName mystring)

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

injectFastInvoker
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)
}
injectInvokeFastInvokerInvoke
Applicator
Applicatorinjectvalues

一个简单的 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
    ...
Contextinject.Injectorinterface{}
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()
}
*macaron.ContextNameGreet()*macaron.Contextrun()
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.ContextInjectorInvoke*MyContextisLogin

具体的实践可以参考 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.BindbindJsonvalidateAndMap
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 框架该有的样子。虽然他在一定程度上使用了过多的反射,但却提高了开发的效率。
文章中如有描述不准确的地方,还请小伙伴们指出。