golang不到30行代碼實現依賴注入

項目地址

go-di-demojava

本項目依賴

使用標準庫實現,無額外依賴mysql

依賴注入的優點

用java的人對於spring框架必定不會陌生,spring核心就是一個IoC(控制反轉/依賴注入)容器,帶來一個很大的優點是解耦。通常只依賴容器,而不依賴具體的類,當你的類有修改時,最多須要改動一下容器相關代碼,業務代碼並不受影響。git

golang的依賴注入原理

總的來講和java的差很少,步驟以下:(golang不支持動態建立對象,因此須要先手動建立對象而後注入,java能夠直接動態建立對象)github

  1. 經過反射讀取對象的依賴(golang是經過tag實現)
  2. 在容器中查找有無該對象實例
  3. 若是有該對象實例或者建立對象的工廠方法,則注入對象或使用工廠建立對象並注入
  4. 若是無該對象實例,則報錯

代碼實現

一個典型的容器實現以下,依賴類型參考了spring的singleton/prototype,分別對象單例對象和實例對象:golang

package di

import (
    "sync"
    "reflect"
    "fmt"
    "strings"
    "errors"
)

var (
    ErrFactoryNotFound = errors.New("factory not found")
)

type factory = func() (interface{}, error)
// 容器
type Container struct {
    sync.Mutex
    singletons map[string]interface{}
    factories  map[string]factory
}
// 容器實例化
func NewContainer() *Container {
    return &Container{
        singletons: make(map[string]interface{}),
        factories:  make(map[string]factory),
    }
}

// 註冊單例對象
func (p *Container) SetSingleton(name string, singleton interface{}) {
    p.Lock()
    p.singletons[name] = singleton
    p.Unlock()
}

// 獲取單例對象
func (p *Container) GetSingleton(name string) interface{} {
    return p.singletons[name]
}

// 獲取實例對象
func (p *Container) GetPrototype(name string) (interface{}, error) {
    factory, ok := p.factories[name]
    if !ok {
        return nil, ErrFactoryNotFound
    }
    return factory()
}

// 設置實例對象工廠
func (p *Container) SetPrototype(name string, factory factory) {
    p.Lock()
    p.factories[name] = factory
    p.Unlock()
}

// 注入依賴
func (p *Container) Ensure(instance interface{}) error {
    elemType := reflect.TypeOf(instance).Elem()
    ele := reflect.ValueOf(instance).Elem()
    for i := 0; i < elemType.NumField(); i++ { // 遍歷字段
        fieldType := elemType.Field(i)
        tag := fieldType.Tag.Get("di") // 獲取tag
        diName := p.injectName(tag)
        if diName == "" {
            continue
        }
        var (
            diInstance interface{}
            err        error
        )
        if p.isSingleton(tag) {
            diInstance = p.GetSingleton(diName)
        }
        if p.isPrototype(tag) {
            diInstance, err = p.GetPrototype(diName)
        }
        if err != nil {
            return err
        }
        if diInstance == nil {
            return errors.New(diName + " dependency not found")
        }
        ele.Field(i).Set(reflect.ValueOf(diInstance))
    }
    return nil
}

// 獲取須要注入的依賴名稱
func (p *Container) injectName(tag string) string {
    tags := strings.Split(tag, ",")
    if len(tags) == 0 {
        return ""
    }
    return tags[0]
}

// 檢測是否單例依賴
func (p *Container) isSingleton(tag string) bool {
    tags := strings.Split(tag, ",")
    for _, name := range tags {
        if name == "prototype" {
            return false
        }
    }
    return true
}

// 檢測是否實例依賴
func (p *Container) isPrototype(tag string) bool {
    tags := strings.Split(tag, ",")
    for _, name := range tags {
        if name == "prototype" {
            return true
        }
    }
    return false
}

// 打印容器內部實例
func (p *Container) String() string {
    lines := make([]string, 0, len(p.singletons)+len(p.factories)+2)
    lines = append(lines, "singletons:")
    for name, item := range p.singletons {
        line := fmt.Sprintf("  %s: %x %s", name, &item, reflect.TypeOf(item).String())
        lines = append(lines, line)
    }
    lines = append(lines, "factories:")
    for name, item := range p.factories {
        line := fmt.Sprintf("  %s: %x %s", name, &item, reflect.TypeOf(item).String())
        lines = append(lines, line)
    }
    return strings.Join(lines, "\n")
}
  1. 最重要的是Ensure方法,該方法掃描實例的全部export字段,並讀取di標籤,若是有該標籤則啓動注入。
  2. 判斷di標籤的類型來肯定注入singleton或者prototype對象

測試

  1. 單例對象在整個容器中只有一個實例,因此無論在何處注入,獲取到的指針必定是同樣的。
  2. 實例對象是經過同一個工廠方法建立的,因此每一個實例的指針不能夠相同。

下面是測試入口代碼,完整代碼在github倉庫,有興趣的能夠翻閱:spring

package main

import (
    "di"
    "database/sql"
    "fmt"
    "os"
    _ "github.com/go-sql-driver/mysql"
    "demo"
)

func main() {
    container := di.NewContainer()
    db, err := sql.Open("mysql", "root:root@tcp(localhost)/sampledb")
    if err != nil {
        fmt.Printf("error: %s\n", err.Error())
        os.Exit(1)
    }
    container.SetSingleton("db", db)
    container.SetPrototype("b", func() (interface{}, error) {
        return demo.NewB(), nil
    })

    a := demo.NewA()
    if err := container.Ensure(a); err != nil {
        fmt.Println(err)
        return
    }
    // 打印指針,確保單例和實例的指針地址
    fmt.Printf("db: %p\ndb1: %p\nb: %p\nb1: %p\n", a.Db, a.Db1, &a.B, &a.B1)
}

執行以後打印出來的結果爲:sql

db: 0xc4200b6140
db1: 0xc4200b6140
b: 0xc4200a0330
b1: 0xc4200a0338

能夠看到兩個db實例的指針同樣,說明是同一個實例,而兩個b的指針不一樣,說明不是一個實例。app

寫在最後

經過依賴注入能夠很好的管理多個對象之間的實例化以及依賴關係,配合配置文件在應用初始化階段將須要注入的實例註冊到容器中,在應用的任何地方只須要在實例化時注入容器便可。沒有額外依賴。框架

我的博客

www.ddhigh.comtcp

相關文章
相關標籤/搜索