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

【導讀】golang 做依賴注入有什麼思路和實現方案?本文中作者用例子做了介紹。

項目地址

go-di-demo https://github.com/xialeistudio/di-demo

本項目依賴

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

依賴注入的優勢

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

golang 的依賴注入原理

總的來說和 java 的差不多,步驟如下:(golang 不支持動態創建對象,所以需要先手動創建對象然後注入,java 可以直接動態創建對象)

  1. 通過反射讀取對象的依賴 (golang 是通過 tag 實現)

  2. 在容器中查找有無該對象實例

  3. 如果有該對象實例或者創建對象的工廠方法,則注入對象或使用工廠創建對象並注入

  4. 如果無該對象實例,則報錯

代碼實現

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

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 倉庫,有興趣的可以翻閱:

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)
}

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

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

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

寫在最後

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

轉自:

segmentfault.com/a/1190000015752299

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/x4L2tmB6lldSAloc1mEefA