go語言依賴注入實現


最近做項目中,生成對象還是使用比較原始的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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM