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