這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
依賴注入(Dependency Injection)和控制反轉(Inversion of Control)是同一個概念。在傳統的程式設計過程中,調用者是自己來決定使用哪些被調用者實現的。但是在依賴注入模式中,建立被調用者的工作不再由調用者來完成,因此稱為控制反轉;建立被調用者執行個體的工作通常由注入器來完成,然後注入調用者,因此也稱為依賴注入。
inject 是依賴注入的golang實現,作者是 codegangsta 。它能在運行時注入參數,調用方法。是Martini架構的基礎核心。
我對依賴注入提取了以下2點性質:
由注入器注入屬性。
由注入器建立被調用者執行個體。
在inject中,被調用者為func,因此注入屬性也即對func注入實參(當然inject也可以注入struct,這樣的話注入的屬性就是struct中的已添加tag為`inject`的匯出欄位)。我們來看下普通的函數調用:
package mainimport ("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 mainimport ("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_testMy 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 {ApplicatorInvokerTypeMapperSetParent(Injector)}type Applicator interface {Apply(interface{}) error}type Invoker interface {Invoke(interface{}) ([]reflect.Value, error)}type TypeMapper interface {Map(interface{}) TypeMapperMapTo(interface{}, interface{}) TypeMapperGet(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.Valueparent 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 mainimport ("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_testinterface {}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 mainimport ("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_teststring is valid? trueSpecialString is valid? trueint 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 Funcfor 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 mainimport ("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 mainimport ("fmt""github.com/codegangsta/inject")type SpecialString interface{}type TestStruct struct {Name string `inject`Nick []byteGender 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_tests.Name = 陳一回s.Gender = 男s.Age = 20
刑星寫了一篇博文可供參考:在Golang中用名字調用函數 ,建議大家都去看下。