go語言依賴注入實現

我沒有領悟發表於2020-05-25

最近做專案中,生成物件還是使用比較原始的New和簡單工廠的方式,使用過程中感覺不太爽快(依賴緊密,有點改動就比較麻煩),還是比較喜歡使用依賴注入的方式。

然後網上沒有找到比較好用的依賴注入包,就自己動手寫了一個,也不要求啥,能用就會,把我從繁瑣的New方法中解脫出來。

先說一下簡單實現原理

  1. 通過反射讀取物件的依賴(golang是通過tag實現)
  2. 在容器中查詢有無該物件例項
  3. 如果有該物件例項或者建立物件的工廠方法,則注入物件或使用工廠建立物件並注入
  4. 如果無該物件例項,則報錯

需要注意的地方:

1、注入的物件首字母需要大寫,小寫的話,在go中代表私有,通過反射無法修改值

2、go反射無法通過讀取配置檔案資訊動態建立物件

 

首先,介紹一下專案層次結構

 

 

 主要解決:資料庫-》倉儲(讀寫分離)-》服務-》控制器 這幾層的依賴注入問題

資料庫,我這裡為了簡化資料庫細節,採用模擬資料的辦法來實現,實際專案中是需要讀取真是資料庫的,程式碼如下

//準備使用者資料,實際開發一般從資料庫讀取
var users []entities.UserEntity

func init() {
    users = append(users, entities.UserEntity{ID: 1, Name: "小明", NickName: "無敵", Gender: 1, Age: 13, Tel: "18886588086", Address: "中國,廣東,深圳"})
    users = append(users, entities.UserEntity{ID: 2, Name: "小紅", NickName: "傻妞", Gender: 0, Age: 13, Tel: "1888658809", Address: "中國,廣東,廣州"})
}

type MockDB struct {
    Host  string
    User  string
    Pwd   string
    Alias string
}

func (db *MockDB) Connect() bool {
    return true
}

func (db *MockDB) Users() []entities.UserEntity {
    return users
}

func (db *MockDB) Close() {

}

資料倉儲,為了實現讀寫分離,分離了兩個介面,例如user倉儲分為i_user_reader和i_user_repository,其中i_user_repository包含i_user_reader(即繼承了i_user_reader)

介面定義如下:

type IUserReader interface {
    GetUsers() []dtos.UserDto
    GetUser(id int64) *dtos.UserDto
    GetMaxUserId() int64
}

type IUserRepository interface {
    IUserReader
    AddUser(user *inputs.UserInput) error
    UpdateUserNickName(id int64, nickName string) error
}

倉儲實現如下:

user_read

type UserRead struct {
    ReadDb *db.MockDB `inject:"MockDBRead"`
}

func (r *UserRead) GetUsers() []dtos.UserDto {
    if r.ReadDb.Connect() {
        users := r.ReadDb.Users()
        var list []dtos.UserDto
        for _, user := range users {
            list = append(list, dtos.UserDto{ID: user.ID, Name: user.Name, NickName: user.NickName, Gender: user.Gender, Age: user.Age, Tel: user.Tel, Address: user.Address})
        }
        return list
    }
    return nil
}

func (r *UserRead) GetUser(id int64) *dtos.UserDto {
    if r.ReadDb.Connect() {
        users := r.ReadDb.Users()
        for _, user := range users {
            if user.ID == id {
                return &dtos.UserDto{ID: user.ID, Name: user.Name, NickName: user.NickName, Gender: user.Gender, Age: user.Age, Tel: user.Tel, Address: user.Address}
            }
        }
        return &dtos.UserDto{}
    }
    return nil
}

func (r *UserRead) GetMaxUserId() int64 {
    var maxId int64
    if r.ReadDb.Connect() {
        users := r.ReadDb.Users()
        for _, user := range users {
            if user.ID > maxId {
                maxId = user.ID
            }
        }
    }
    return maxId
}
UserRepository:
type UserRepository struct {
    UserRead
    WriteDb *db.MockDB `inject:"MockDBWrite"`
}

func (w *UserRepository) AddUser(user *inputs.UserInput) error {
    model := entities.UserEntity{}
    model.ID = w.GetMaxUserId() + 1
    model.Name = user.Name
    model.NickName = user.NickName
    model.Gender = user.Gender
    model.Age = user.Age
    model.Address = user.Address
    if w.ReadDb.Connect() {
        users := w.ReadDb.Users()
        users = append(users, model)
    }
    return nil
}

func (w *UserRepository) UpdateUserNickName(id int64, nickName string) error {
    user := w.GetUser(id)
    if user.ID > 0 {
        user.NickName = nickName
        return nil
    } else {
        return errors.New("未找到使用者資訊")
    }
}

注意,user_read依賴注入的是讀db:ReadDB,user_repository依賴注入的是寫db:WriteDB

 

服務的介面和實現

i_user_service:

type IUserService interface {
    GetUsers() []dtos.UserDto
    GetUser(id int64) *dtos.UserDto
    AddUser(user *inputs.UserInput) error
}

user_service:

type UserService struct {
    UserRepository repositories.IUserRepository `inject:"UserRepository"`
}

func (s *UserService) AddUser(user *inputs.UserInput) error {
    return s.UserRepository.AddUser(user)
}

func (s *UserService) GetUsers() []dtos.UserDto {
    return s.UserRepository.GetUsers()
}

func (s *UserService) GetUser(id int64) *dtos.UserDto {
    return s.UserRepository.GetUser(id)
}

UserService依賴注入UserRepository,另外,專案中,特意把倉儲介面定義和服務放在同一層,是為了讓服務只依賴倉儲介面,不依賴倉儲具體實現。這算是設計模式原則的依賴倒置原則的體現吧。

控制器實現:

type UserController struct {
    UserService user.IUserService `inject:"UserService"`
}

func (ctrl *UserController) GetUsers(ctx *gin.Context) {
    users := ctrl.UserService.GetUsers()
    Ok(Response{Code: Success, Msg: "獲取使用者成功!", Data: users}, ctx)
}

func (ctrl *UserController) GetUser(ctx *gin.Context) {
    idStr := ctx.Param("id")
    id, err := strconv.ParseInt(idStr, 10, 64)
    if err != nil {
        BadRequestError("id引數格式錯誤", ctx)
        return
    }
    users := ctrl.UserService.GetUser(id)
    Ok(Response{Code: Success, Msg: "獲取使用者成功!", Data: users}, ctx)
}

func (ctrl *UserController) AddUser(ctx *gin.Context) {
    input := inputs.UserInput{}
    err := ctx.ShouldBindJSON(&input)
    if err != nil {
        BadRequestError("引數錯誤", ctx)
        return
    }
    err = ctrl.UserService.AddUser(&input)
    if err != nil {
        Ok(Response{Code: Failed, Msg: err.Error()}, ctx)
        return
    }
    Ok(Response{Code: Success, Msg: "新增使用者成功!"}, ctx)
}

UserController依賴注入UserService

接下來是實現依賴注入的核心程式碼,容器的實現

Container:

var injectTagName = "inject" //依賴注入tag名

//生命週期
// singleton:單例 單一例項,每次使用都是該例項
// transient:瞬時例項,每次使用都建立新的例項
type Container struct {
    sync.Mutex
    singletons map[string]interface{}
    transients map[string]factory
}

type factory = func() (interface{}, error)

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

func (c *Container) GetSingleton(name string) interface{} {
    return c.singletons[name]
}

//註冊瞬時例項建立工廠方法
func (c *Container) SetTransient(name string, factory factory) {
    c.Lock()
    c.transients[name] = factory
    c.Unlock()
}

func (c *Container) GetTransient(name string) interface{} {
    factory := c.transients[name]
    instance, _ := factory()
    return instance
}

//注入例項
func (c *Container) Entry(instance interface{}) error {
    err := c.entryValue(reflect.ValueOf(instance))
    if err != nil {
        return err
    }
    return nil
}

func (c *Container) entryValue(value reflect.Value) error {
    if value.Kind() != reflect.Ptr {
        return errors.New("必須為指標")
    }
    elemType, elemValue := value.Type().Elem(), value.Elem()
    for i := 0; i < elemType.NumField(); i++ {
        if !elemValue.Field(i).CanSet() { //不可設定 跳過
            continue
        }

        fieldType := elemType.Field(i)
        if fieldType.Anonymous {
            //fmt.Println(fieldType.Name + "是匿名欄位")
            item := reflect.New(elemValue.Field(i).Type())
            c.entryValue(item) //遞迴注入
            elemValue.Field(i).Set(item.Elem())
        } else {
            if elemValue.Field(i).IsZero() { //零值才注入
                //fmt.Println(elemValue.Field(i).Interface())
                //fmt.Println(fieldType.Name)
                tag := fieldType.Tag.Get(injectTagName)
                injectInstance, err := c.getInstance(tag)
                if err != nil {
                    return err
                }
                c.entryValue(reflect.ValueOf(injectInstance)) //遞迴注入

                elemValue.Field(i).Set(reflect.ValueOf(injectInstance))
            } else {
                fmt.Println(fieldType.Name)
            }
        }
    }
    return nil
}

func (c *Container) getInstance(tag string) (interface{}, error) {
    var injectName string
    tags := strings.Split(tag, ",")
    if len(tags) == 0 {
        injectName = ""
    } else {
        injectName = tags[0]
    }

    if c.isTransient(tag) {
        factory, ok := c.transients[injectName]
        if !ok {
            return nil, errors.New("transient factory not found")
        } else {
            return factory()
        }
    } else { //預設單例
        instance, ok := c.singletons[injectName]
        if !ok || instance == nil {
            return nil, errors.New(injectName + " dependency not found")
        } else {
            return instance, nil
        }
    }
}

// transient:瞬時例項,每次使用都建立新的例項
func (c *Container) isTransient(tag string) bool {
    tags := strings.Split(tag, ",")
    for _, name := range tags {
        if name == "transient" {
            return true
        }
    }
    return false
}

func (c *Container) String() string {
    lines := make([]string, 0, len(c.singletons)+len(c.transients)+2)
    lines = append(lines, "singletons:")
    for key, value := range c.singletons {
        line := fmt.Sprintf("    %s: %x %s", key, c.singletons[key], reflect.TypeOf(value).String())
        lines = append(lines, line)
    }

    lines = append(lines, "transients:")
    for key, value := range c.transients {
        line := fmt.Sprintf("    %s: %x %s", key, c.transients[key], reflect.TypeOf(value).String())
        lines = append(lines, line)
    }
    return strings.Join(lines, "\n")
}

這裡使用了兩種生命週期的例項:單例和瞬時(其他生命週期,水平有限哈)

簡單說下原理,容器主要包含兩個map物件,用來儲存物件和建立對方方法,然後依賴注入實現,就是通過反射獲取tag資訊,再去容器map中獲取物件,通過反射把獲取的物件賦值到欄位中。

我這裡採用了遞迴注入的方式,所以本專案中,只用注入UserController物件即可,因為實際專案中多點是有多個Controller物件,所以我這裡使用了個簡單工廠來建立Controller物件,然後只用注入工廠方法即可

工廠方法實現如下:

type CtrlFactory struct {
    UserCtrl *controllers.UserController `inject:"UserController"`
}

使用容器前,需要先初始化好容器物件,這裡使用一個全域性物件,然後初始化好需要注入的物件,實現程式碼如下:

var GContainer = &Container{
    singletons: make(map[string]interface{}),
    transients: make(map[string]factory),
}

func Init() {
    //db
    GContainer.SetSingleton("MockDBRead", &db.MockDB{Host: "192.168.1.12:3036", User: "root", Pwd: "123456", Alias: "Read"})
    GContainer.SetSingleton("MockDBWrite", &db.MockDB{Host: "192.168.1.25:3036", User: "root", Pwd: "123456", Alias: "Write"})

    //倉儲
    GContainer.SetSingleton("UserRepository", &user.UserRepository{})

    //服務
    GContainer.SetSingleton("UserService", &userDomain.UserService{})

    //控制器
    GContainer.SetSingleton("UserController", &controllers.UserController{})

    //控制器工廠
    ctlFactory := &CtrlFactory{}
    GContainer.SetSingleton("CtrlFactory", ctlFactory)

    GContainer.Entry(ctlFactory) //注入

    fmt.Println(GContainer.String())
}

依賴注入程式碼實現講完了,然後就是具體使用了,使用時,先在main方法中呼叫容器出事化方法Init() (注意,這裡Init特意大寫,要和go包的init區分,go包的init是自動呼叫,這裡大寫的Init是需要手動呼叫的,至於為啥呢,注意是可以控制呼叫時機,go包的init呼叫順序有點莫名其妙,特別是包引用複雜的時候),main程式碼如下:

func main() {
    Init()
    Run()
}

func Init() {
    inject.Init()
}

func Run() {
    router := router.Init()

    s := &http.Server{
        Addr:           ":8080",
        Handler:        router,
        ReadTimeout:    time.Duration(10) * time.Second,
        WriteTimeout:   time.Duration(10) * time.Second,
        MaxHeaderBytes: 1 << 20,
    }
    go func() {
        log.Println("Server Listen at:8080")
        if err := s.ListenAndServe(); err != nil {
            log.Printf("Listen:%s\n", err)
        }
    }()

    quit := make(chan os.Signal)
    signal.Notify(quit, os.Interrupt)
    <-quit

    log.Println("Shutdown Server...")
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    if err := s.Shutdown(ctx); err != nil {
        log.Fatal("Server Shutdown:", err)
    }
    log.Println("Server exiting")
}

我這裡使用了gin框架來構建http服務

初始化話完畢後,就是在路由中使用controller了,先從容器中獲取工廠物件,然後通過go型別推斷轉化為具體型別,程式碼如下:

func Init() *gin.Engine {
    // Creates a router without any middleware by default
    r := gin.New()
    r.Use(gin.Logger())
    // Recovery middleware recovers from any panics and writes a 500 if there was one.
    r.Use(gin.Recovery())

    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })

    factory := inject.GContainer.GetSingleton("CtrlFactory")
    ctrlFactory := factory.(*inject.CtrlFactory)

    apiV1 := r.Group("/api/v1")
    //users
    userRg := apiV1.Group("/user")
    {
        userRg.POST("", ctrlFactory.UserCtrl.AddUser)
        userRg.GET("", ctrlFactory.UserCtrl.GetUsers)
        userRg.GET("/:id", ctrlFactory.UserCtrl.GetUser)
    }

    gin.SetMode("debug")
    return r
}

核心程式碼就是:

factory := inject.GContainer.GetSingleton("CtrlFactory")
ctrlFactory := factory.(*inject.CtrlFactory)

ok,介紹完了。初始弄這個依賴注入可能覺得有點麻煩,但這是一勞永逸的辦法,後面有啥增加修改的就比較簡單

具體程式碼放在github上了,有興趣可以關注一下:https://github.com/marshhu/ma-inject

 

相關文章