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

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

随便写写 2880 字 / 6 分钟

前阵子本来打算重构博客,但是后来用 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 框架该有的样子。虽然他在一定程度上使用了过多的反射,但却提高了开发的效率。 文章中如有描述不准确的地方,还请小伙伴们指出。