文档章节

golang: Martini之inject源码分析

陈亦
 陈亦
发布于 2014/01/22 15:23
字数 2412
阅读 6978
收藏 42

依赖注入(Dependency Injection)和控制反转(Inversion of Control)是同一个概念。在传统的程序设计过程中,调用者是自己来决定使用哪些被调用者实现的。但是在依赖注入模式中,创建被调用者的工作不再由调用者来完成,因此称为控制反转;创建被调用者实例的工作通常由注入器来完成,然后注入调用者,因此也称为依赖注入。

inject 是依赖注入的golang实现,作者是 codegangsta 。它能在运行时注入参数,调用方法。是Martini框架的基础核心。

我对依赖注入提取了以下2点性质:

  1. 由注入器注入属性。

  2. 由注入器创建被调用者实例。

在inject中,被调用者为func,因此注入属性也即对func注入实参(当然inject也可以注入struct,这样的话注入的属性就是struct中的已添加tag为`inject`的导出字段)。我们来看下普通的函数调用:

package main

import (
	"fmt"
)

func Say(name, gender string, age int) {
	fmt.Printf("My name is %s, gender is %s, age is %d!\n", name, gender, age)
}

func main() {
	Say("陈一回", "男", 20)
}

上面的例子中,定义了函数Say并在main方法中手动调用。这样总是可行的,但是有时候我们不得不面对这样一种情况:比如在web开发中,我们注册路由,服务器接受请求,然后根据request path调用相应的handler。这个handler必然不是由我们手动来调用的,而是由服务器端根据路由匹配来查找对应的handler并自动调用。

是时候引入inject了,尝试用inject改写上面的代码:

package main

import (
	"fmt"
	"github.com/codegangsta/inject"
)

type SpecialString interface{}

func Say(name string, gender SpecialString, age int) {
	fmt.Printf("My name is %s, gender is %s, age is %d!\n", name, gender, age)
}

func main() {
	inj := inject.New()
	inj.Map("陈一回")
	inj.MapTo("男", (*SpecialString)(nil))
	inj.Map(20)
	inj.Invoke(Say)
}

$ cd $GOPATH/src/injector_test
$ go build
$ ./injector_test
My name is 陈一回, gender is 男, age is 20!

看不懂?没关系,因为我们对于inject还没有足够的知识储备,一切从分析inject的源码开始。

inject包只有2个文件,一个是inject.go文件,还有一个是inject_test.go,但我们只关注inject.go文件。

inject.go短小精悍,包括注释和空行才157行。定义了4个接口,包括一个父接口和三个子接口,接下来您就会知道这样定义的好处了。

为了方便,我把所有的注释都去掉了:

type Injector interface {
	Applicator
	Invoker
	TypeMapper
	SetParent(Injector)
}

type Applicator interface {
	Apply(interface{}) error
}

type Invoker interface {
	Invoke(interface{}) ([]reflect.Value, error)
}

type TypeMapper interface {
	Map(interface{}) TypeMapper
	MapTo(interface{}, interface{}) TypeMapper
	Get(reflect.Type) reflect.Value
}

接口Injector是接口Applicator、接口Invoker、接口TypeMapper的父接口,所以实现了Injector接口的类型,也必然实现了Applicator接口、Invoker接口和TypeMapper接口。

Applicator接口只规定了Apply成员,它用于注入struct。

Invoker接口只规定了Invoke成员,它用于执行被调用者。

TypeMapper接口规定了三个成员,Map和MapTo都用于注入参数,但它们有不同的用法。Get用于调用时获取被注入的参数。

另外Injector还规定了SetParent行为,它用于设置父Injector,其实它相当于查找继承。也即通过Get方法在获取被注入参数时会一直追溯到parent,这是个递归过程,直到查找到参数或为nil终止。

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

func InterfaceOf(value interface{}) reflect.Type {
	t := reflect.TypeOf(value)

	for t.Kind() == reflect.Ptr {
		t = t.Elem()
	}

	if t.Kind() != reflect.Interface {
		panic("Called inject.InterfaceOf with a value that is not a pointer to an interface. (*MyInterface)(nil)")
	}

	return t
}

func New() Injector {
	return &injector{
		values: make(map[reflect.Type]reflect.Value),
	}
}

injector是inject包中唯一定义的struct,所有的操作都是基于injector struct来进行的。它有两个成员values和parent。values用于保存注入的参数,它是一个用reflect.Type当键、reflect.Value为值的map,这个很重要,理解这点将有助于理解Map和MapTo。New方法用于初始化injector struct,并返回一个指向injector struct的指针。但是请注意这个返回值被Injector接口包装了。

InterfaceOf方法虽然只有几句实现代码,但它是Injector的核心。InterfaceOf方法的参数必须是一个接口类型的指针,如果不是则引发panic。InterfaceOf方法的返回类型是reflect.Type,您应该还记得injector的成员values就是一个reflect.Type类型当键的map。这个方法的作用其实只是获取参数的类型,而不关心它的值。我之前有篇文章介绍过(*interface{})(nil),感兴趣的朋友可以去看看:golang: 详解interface和nil

为了加深理解,来举个例子:

package main

import (
	"fmt"
	"github.com/codegangsta/inject"
)

type SpecialString interface{}

func main() {
	fmt.Println(inject.InterfaceOf((*interface{})(nil)))
	fmt.Println(inject.InterfaceOf((*SpecialString)(nil)))
}

$ cd $GOPATH/src/injector_test
$ go build
$ ./injector_test
interface {}
main.SpecialString

上面的输出一点也不奇怪。InterfaceOf方法就是用来得到参数类型,而不关心它具体存储的是什么值。值得一提的是,我们定义了一个SpecialString接口。我们在之前的代码也有定义SpecialString接口,用在Say方法的参数声明中,之后您就会知道为什么要这么做。当然您不一定非得命名为SpecialString。

func (i *injector) Map(val interface{}) TypeMapper {
	i.values[reflect.TypeOf(val)] = reflect.ValueOf(val)
	return i
}

func (i *injector) MapTo(val interface{}, ifacePtr interface{}) TypeMapper {
	i.values[InterfaceOf(ifacePtr)] = reflect.ValueOf(val)
	return i
}

func (i *injector) Get(t reflect.Type) reflect.Value {
	val := i.values[t]
	if !val.IsValid() && i.parent != nil {
		val = i.parent.Get(t)
	}
	return val
}

func (i *injector) SetParent(parent Injector) {
	i.parent = parent
}

Map和MapTo方法都用于注入参数,保存于injector的成员values中。这两个方法的功能完全相同,唯一的区别就是Map方法用参数值本身的类型当键,而MapTo方法有一个额外的参数可以指定特定的类型当键。但是MapTo方法的第二个参数ifacePtr必须是接口指针类型,因为最终ifacePtr会作为InterfaceOf方法的参数。

为什么需要有MapTo方法?因为注入的参数是存储在一个以类型为键的map中,可想而知,当一个函数中有一个以上的参数的类型是一样时,后执行Map进行注入的参数将会覆盖前一个通过Map注入的参数。

SetParent方法用于给某个Injector指定父Injector。Get方法通过reflect.Type从injector的values成员中取出对应的值,它可能会检查是否设置了parent,直到找到或返回无效的值,最后Get方法的返回值会经过IsValid方法的校验。举个例子来加深理解:

package main

import (
	"fmt"
	"github.com/codegangsta/inject"
	"reflect"
)

type SpecialString interface{}

func main() {
	inj := inject.New()
	inj.Map("陈一回")
	inj.MapTo("男", (*SpecialString)(nil))
	inj.Map(20)
	fmt.Println("string is valid?", inj.Get(reflect.TypeOf("姓陈名一回")).IsValid())
	fmt.Println("SpecialString is valid?", inj.Get(inject.InterfaceOf((*SpecialString)(nil))).IsValid())
	fmt.Println("int is valid?", inj.Get(reflect.TypeOf(18)).IsValid())
	fmt.Println("[]byte is valid?", inj.Get(reflect.TypeOf([]byte("Golang"))).IsValid())
	inj2 := inject.New()
	inj2.Map([]byte("test"))
	inj.SetParent(inj2)
	fmt.Println("[]byte is valid?", inj.Get(reflect.TypeOf([]byte("Golang"))).IsValid())
}

$ cd $GOPATH/src/injector_test
$ go build
$ ./injector_test
string is valid? true
SpecialString is valid? true
int is valid? true
[]byte is valid? false
[]byte is valid? true

通过以上例子应该知道SetParent是什么样的行为。是不是很像面向对象中的查找链?

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方法用于动态执行函数,当然执行前可以通过Map或MapTo来注入参数,因为通过Invoke执行的函数会取出已注入的参数,然后通过reflect包中的Call方法来调用。Invoke接收的参数f是一个接口类型,但是f的底层类型必须为func,否则会panic。

package main

import (
	"fmt"
	"github.com/codegangsta/inject"
)

type SpecialString interface{}

func Say(name string, gender SpecialString, age int) {
	fmt.Printf("My name is %s, gender is %s, age is %d!\n", name, gender, age)
}

func main() {
	inj := inject.New()
	inj.Map("陈一回")
	inj.MapTo("男", (*SpecialString)(nil))
	inj2 := inject.New()
	inj2.Map(20)
	inj.SetParent(inj2)
	inj.Invoke(Say)
}

上面的例子如果没有定义SpecialString接口作为gender参数的类型,而把name和gender都定义为string类型,那么gender会覆盖name的值。如果您还没有明白,建议您把这篇文章从头到尾再看几遍。

func (inj *injector) Apply(val interface{}) error {
	v := reflect.ValueOf(val)

	for v.Kind() == reflect.Ptr {
		v = v.Elem()
	}

	if v.Kind() != reflect.Struct {
		return nil
	}

	t := v.Type()

	for i := 0; i < v.NumField(); i++ {
		f := v.Field(i)
		structField := t.Field(i)
		if f.CanSet() && structField.Tag == "inject" {
			ft := f.Type()
			v := inj.Get(ft)
			if !v.IsValid() {
				return fmt.Errorf("Value not found for type %v", ft)
			}

			f.Set(v)
		}

	}

	return nil
}

Apply方法是用于对struct的字段进行注入,参数为指向底层类型为结构体的指针。可注入的前提是:字段必须是导出的(也即字段名以大写字母开头),并且此字段的tag设置为`inject`。以例子来说明:

package main

import (
	"fmt"
	"github.com/codegangsta/inject"
)

type SpecialString interface{}
type TestStruct struct {
	Name   string `inject`
	Nick   []byte
	Gender SpecialString `inject`
	uid    int           `inject`
	Age    int           `inject`
}

func main() {
	s := TestStruct{}
	inj := inject.New()
	inj.Map("陈一回")
	inj.MapTo("男", (*SpecialString)(nil))
	inj2 := inject.New()
	inj2.Map(20)
	inj.SetParent(inj2)
	inj.Apply(&s)
	fmt.Println("s.Name =", s.Name)
	fmt.Println("s.Gender =", s.Gender)
	fmt.Println("s.Age =", s.Age)
}

$ cd $GOPATH/src/injector_test
$ go build
$ ./injector_test
s.Name = 陈一回
s.Gender = 男
s.Age = 20

刑星写了一篇博文可供参考:在Golang中用名字调用函数 ,建议大家都去看下。

© 著作权归作者所有

共有 人打赏支持
陈亦
粉丝 236
博文 23
码字总数 53194
作品 0
浦东
高级程序员
私信 提问
加载中

评论(21)

logbird
logbird
非常好的文章赞一个
林希
林希
最佩服的就是用简单的话语将道理讲明白的人,这不是一件容易的事情,真正做到的人就一定是一个thinker.
谢谢
Nichole
Nichole
我刚刚测试了一下 原来是因为大小写的关系,如果改成Uid,那Uid也会绑定成20。
这个只能绑定结构体大写字母开头的值。
Nichole
Nichole
最后一个例子中 TestStruct 有两个int类型的值,为什么他绑定的是 Age 而不是 uid?
Injection
Injection
这个应该不算注入 算是binder吧
jellycool
jellycool
第二次看,赞+13
ooz
ooz
mark
陈亦
陈亦

引用来自“无闻”的评论

第2次看,继续赞。

额~过奖了0
无闻
无闻
第2次看,继续赞。
随便乱打的

引用来自“陈一回”的评论

引用来自“随便乱打的”的评论

博主你好,我刚开始学习Golang,接口方面还是不是很明白,想向您请教一个问题:在这个源码中 type Injector interface 接口的意义是什么?主要是起到松耦合的作用吧?还是有其他作用?因为源码中只有 type injector struct 实现了这个接口,如果只使用 type injector struct ,而不设计接口,应该也可以实现相同的功能吧。?谢谢

接口的用途在不同的语言中都是差不多的。这个inject主要是用于martini项目,你可以去看下inject在martini中的用法。https://github.com/codegangsta/martini

好的。。谢谢!
Martini 极好的 Go WEB 框架

已知的其他框架看到的是传统OOP的影子, 到处充蚀 Class 风格的 OOP 方法. 而我们知道GoLang中是没有Class的. 笔者也曾努力用Go 的风格做WEB开发, 总感到力不从心. 写出的代码不能完全称之为框...

喻恒春
2014/01/07
0
16
cordova-hcp server 报错

Running serverCould not create tunnel: Error: panic: runtime error: invalid memory address or nil pointer dereference github.com/inconshreveable/olive/recover.go:40runtime/asm_a......

mamie42
2016/10/17
121
0
JesusSlim/pinject

pinject Inject in PHP ! PHP依赖注入实现. usage English [Chinese] Install pinject in packagist:https://packagist.org/packages/jesusslim/pinject Install: composer require jesussl......

JesusSlim
2016/08/16
0
0
Python/Ruby/Go/Node 之四国大战

Python Flask vs Ruby Sinatra vs Go Martini vs Node Express 本文授权转载自 zybuluo 博客。 题外话一:最近一段时间,Cloud Insight 接连发布了三种语言(Python, Node, Ruby)的SDK,Clo...

OneAPM蓝海讯通
2016/03/17
136
0
技术晨读_20160217

技术导读 Build a RESTful API with Martini 使用martini搭建一个Restful API,使用的是简易的内存database,搭建了一套支持json和xml的RESTFUL的API http://0value.com/build-a-restful-API...

王二狗子11
01/07
0
0

没有更多内容

加载失败,请刷新页面

加载更多

大数据教程(6.1)hadoop生态圈介绍及就业前景

1. HADOOP背景介绍 1.1、什么是HADOOP 1.HADOOP是apache旗下的一套开源软件平台 2.HADOOP提供的功能:利用服务器集群,根据用户的自定义业务逻辑,对海量数据进行分布式处理 3.HADOOP的核心组...

em_aaron
54分钟前
1
0
hadoop垃圾回收站

在生产生,hdfs回收站必须是开启的,一般设置为7天。 fs.trash.interval 为垃圾回收站保留时间,如果为0则禁用回收站功能。 fs.trash.checkpoint.interval 回收站检查点时间,一般设置为小于...

hnairdb
昨天
1
0
腾讯与Github的魔幻会面背后的故事…

10月22日,腾讯开源管理办公室有幸邀请到Github新晋CEO Nat Friedman,前来鹅厂参观交流。目前腾讯已经有近70个项目在Github上开源,共获得17w stars,世界排名11位。Github是腾讯开源的主阵...

腾讯开源
昨天
9
0
单例模式

单例模式(Singleton pattern)属于创建型设计模式。 保证一个类仅有一个实例,并提供一个访问它的全局访问点。 通常我们可以让一个全局变量使得一个对象被访问,但它不能防止你实例化多个对...

NinjaFrog
昨天
2
0
TypeScript基础入门之装饰器(三)

转载 TypeScript基础入门之装饰器(三) 继续上篇文章[TypeScript基础入门之装饰器(二)] 访问器装饰器 Accessor Decorator在访问器声明之前声明。 访问器装饰器应用于访问器的属性描述符,可用...

durban
昨天
3
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部