不到 30 行代碼實現 golang 依賴注入
【導讀】golang 做依賴注入有什麼思路和實現方案?本文中作者用例子做了介紹。
項目地址
go-di-demo https://github.com/xialeistudio/di-demo
本項目依賴
使用標準庫實現,無額外依賴
依賴注入的優勢
用 java 的人對於 spring 框架一定不會陌生,spring 核心就是一個 IoC(控制反轉 / 依賴注入) 容器,帶來一個很大的優勢是解耦。一般只依賴容器,而不依賴具體的類,當你的類有修改時,最多需要改動一下容器相關代碼,業務代碼並不受影響。
golang 的依賴注入原理
總的來說和 java 的差不多,步驟如下:(golang 不支持動態創建對象,所以需要先手動創建對象然後注入,java 可以直接動態創建對象)
-
通過反射讀取對象的依賴 (golang 是通過 tag 實現)
-
在容器中查找有無該對象實例
-
如果有該對象實例或者創建對象的工廠方法,則注入對象或使用工廠創建對象並注入
-
如果無該對象實例,則報錯
代碼實現
一個典型的容器實現如下,依賴類型參考了 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")
}
-
最重要的是
Ensure
方法,該方法掃描實例的所有 export 字段,並讀取 di 標籤,如果有該標籤則啓動注入。 -
判斷 di 標籤的類型來確定注入 singleton 或者 prototype 對象
測試
-
單例對象在整個容器中只有一個實例,所以不管在何處注入,獲取到的指針一定是一樣的。
-
實例對象是通過同一個工廠方法創建的,所以每個實例的指針不可以相同。
下面是測試入口代碼,完整代碼在 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