golang: Martini之inject源碼分析

依賴注入(Dependency Injection)和控制反轉(Inversion of Control)是同一個概念。在傳統的程序設計過程當中,調用者是本身來決定使用哪些被調用者實現的。可是在依賴注入模式中,建立被調用者的工做再也不由調用者來完成,所以稱爲控制反轉;建立被調用者實例的工做一般由注入器來完成,而後注入調用者,所以也稱爲依賴注入。git

inject 是依賴注入的golang實現,做者是 codegangsta 。它能在運行時注入參數,調用方法。是Martini框架的基礎核心。github

我對依賴注入提取了如下2點性質:golang

  1. 由注入器注入屬性。web

  2. 由注入器建立被調用者實例。shell

在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並自動調用。
app

是時候引入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文件。ui

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中用名字調用函數 ,建議你們都去看下。

相關文章
相關標籤/搜索