最近做專案中,生成物件還是使用比較原始的New和簡單工廠的方式,使用過程中感覺不太爽快(依賴緊密,有點改動就比較麻煩),還是比較喜歡使用依賴注入的方式。
然後網上沒有找到比較好用的依賴注入包,就自己動手寫了一個,也不要求啥,能用就會,把我從繁瑣的New方法中解脫出來。
先說一下簡單實現原理
- 通過反射讀取物件的依賴(golang是通過tag實現)
- 在容器中查詢有無該物件例項
- 如果有該物件例項或者建立物件的工廠方法,則注入物件或使用工廠建立物件並注入
- 如果無該物件例項,則報錯
需要注意的地方:
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