文档章节

golang: Martini之inject源码分析

陈亦
 陈亦
发布于 2014/01/22 15:23
字数 2412
阅读 7087
收藏 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中用名字调用函数 ,建议大家都去看下。

© 著作权归作者所有

共有 人打赏支持
陈亦
粉丝 237
博文 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 server Could not create tunnel: Error: panic: runtime error: invalid memory address or nil pointer dereference github.com/inconshreveable/olive/recover.go:40 runtime/asm......

mamie42
2016/10/17
209
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

没有更多内容

加载失败,请刷新页面

加载更多

RestClientUtil和ConfigRestClientUtil区别说明

RestClientUtil directly executes the DSL defined in the code. ConfigRestClientUtil gets the DSL defined in the configuration file by the DSL name and executes it. RestClientUtil......

bboss
今天
12
0

中国龙-扬科
昨天
2
0
Linux系统设置全局的默认网络代理

更改全局配置文件/etc/profile all_proxy="all_proxy=socks://rahowviahva.ml:80/"ftp_proxy="ftp_proxy=http://rahowviahva.ml:80/"http_proxy="http_proxy=http://rahowviahva.ml:80/"......

临江仙卜算子
昨天
9
0
java框架学习日志-6(bean作用域和自动装配)

本章补充bean的作用域和自动装配 bean作用域 之前提到可以用scope来设置单例模式 <bean id="type" class="cn.dota2.tpye.Type" scope="singleton"></bean> 除此之外还有几种用法 singleton:......

白话
昨天
8
0
在PC上测试移动端网站和模拟手机浏览器的5大方法

总结很全面,保存下来以备不时之需。原文地址:https://www.cnblogs.com/coolfeng/p/4708942.html

kitty1116
昨天
7
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部