从 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
类型的参数,那么就会从容器中取出string
和int
类型的变量传入函数中。
这其实就是所谓的“控制反转”,被调用者不再受制于调用者的入参约定,而是调用者来设法满足被调用者。
再简单点说:使用依赖注入可以实现调用任意入参的函数,它会自己找数据注入填充。
依赖注入的 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 是获取到的数据的值。
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()
通过参数类型获得参数的值。最后使用反射调用函数。
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.Context
的run()
方法即为调用 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.Context
的Injector
接口中的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
方法来指定需要绑定的表单结构体。简单地跟一下代码:
- 请求的 body 被传入
bind
方法中(binding.go L104) - 这里以传入 JSON 为例,进入
Json
方法解析 body(binding.go L45) - 方法的最后
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 框架该有的样子。虽然他在一定程度上使用了过多的反射,但却提高了开发的效率。 文章中如有描述不准确的地方,还请小伙伴们指出。
喜欢这篇文章?为什么不打赏一下呢?