简体   繁体   中英

How to implement dependency injection in Go

I'm porting an app from Play (Scala) to Go and wondering how to implement dependency injection. In Scala I used the cake pattern, while in Go I implemented a DAO interface along with an implementation for Mongo.

Here below is how I tried to implement a pattern that let me change the DAO implementation as needed (eg test, different DB, etc.):

1. entity.go

package models

import (
    "time"
    "gopkg.in/mgo.v2/bson"
)

type (
    Entity struct {
        Id        bson.ObjectId `json:"id,omitempty" bson:"_id,omitempty"`
        CreatedAt time.Time     `json:"createdAt,omitempty" bson:"createdAt,omitempty"`
        LastUpdate time.Time    `json:"lastUpdate,omitempty" bson:"lastUpdate,omitempty"`
    }
)

2. user.go

package models

import (
    "time"
)

type (
    User struct {
        Entity                  `bson:",inline"`
        Name      string        `json:"name,omitempty" bson:"name,omitempty"`
        BirthDate time.Time     `json:"birthDate,omitempty" bson:"birthDate,omitempty"`
    }
)

3. dao.go

package persistence

type (
    DAO interface {
        Insert(entity interface{}) error
        List(result interface{}, sort string) error
        Find(id string, result interface{}) error
        Update(id string, update interface{}) error
        Remove(id string) error
        Close()
    }

    daoFactory func() DAO
)

var (
    New daoFactory
)

4. mongoDao.go (DB info and collection name are hard-coded since it's just an example)

package persistence

import (
    "fmt"
    "time"
    "errors"
    "gopkg.in/mgo.v2"
    "gopkg.in/mgo.v2/bson"
    "github.com/fatih/structs"

    "cmd/server/models"
)

type (
    mongoDAO struct{
        session *mgo.Session
    }
)

func NewMongoDAO() DAO {
    dialInfo := &mgo.DialInfo{
        Addrs:      []string{"localhost:27017"},
        Timeout:    60 * time.Second,
        Database:   "test",
    }

    session, err := mgo.DialWithInfo(dialInfo)

    if err != nil {
        panic(err)
    }

    session.SetMode(mgo.Monotonic, true)

    return &mongoDAO{session}
}

func (dao *mongoDAO) Insert(entity interface{}) error {

    doc := entity.(*models.User)
    doc.Id = bson.NewObjectId()
    doc.CreatedAt = time.Now().UTC()
    doc.LastUpdate = time.Now().UTC()

    return dao.session.DB("test").C("users").Insert(doc)
}

func (dao *mongoDAO) List(result interface{}, sort string) error {
    return dao.session.DB("test").C("users").Find(nil).Sort(sort).All(result)
}

func (dao *mongoDAO) Find(id string, result interface{}) error {
    if !bson.IsObjectIdHex(id) {
        return errors.New(fmt.Sprintf("%s is not a valid hex id", id))
    }

    oid := bson.ObjectIdHex(id)

    return dao.session.DB("test").C("users").FindId(oid).One(result)
}

func (dao *mongoDAO) Update(id string, update interface{}) error {
    if !bson.IsObjectIdHex(id) {
        return errors.New(fmt.Sprintf("%s is not a valid hex id", id))
    }

    oid := bson.ObjectIdHex(id)

    doc := update.(*models.User)
    doc.LastUpdate = time.Now().UTC()

    return dao.session.DB("test").C("users").Update(oid, bson.M{"$set": structs.Map(update)})
}

func (dao *mongoDAO) Remove(id string) error {
    if !bson.IsObjectIdHex(id) {
        return errors.New(fmt.Sprintf("%s is not a valid hex id", id))
    }

    oid := bson.ObjectIdHex(id)

    return dao.session.DB("test").C("users").RemoveId(oid)
}

func (dao *mongoDAO) Close() {
    dao.session.Close()
}

func init() {
    New = NewMongoDAO
}

Finally, here is how I use the types above:

5. userController.go

package controllers

import (
    "net/http"
    "github.com/labstack/echo"

    "cmd/server/models"
    "cmd/server/persistence"
)

type (
    UserController struct {
        dao persistence.DAO
    }
)

func NewUserController(dao persistence.DAO) *UserController {
    return &UserController{dao}
}

func (userController *UserController) CreateUser() echo.HandlerFunc {
    return func(context echo.Context) error {
        user := &models.User{}

        if err := context.Bind(user); err != nil {
            return err
        }

        if err := userController.dao.Insert(user); err != nil {
            return err
        }

        return context.JSON(http.StatusCreated, user)
    }
}

func (userController *UserController) UpdateUser() echo.HandlerFunc {
    return func(context echo.Context) error {
        user := &models.User{}

        if err := context.Bind(user); err != nil {
            return err
        }

        id := context.Param("id")
        if err := userController.dao.Update(id, user); err != nil {
            return err
        }

        return context.JSON(http.StatusOK, user)
    }
}

....

The code above is 90% fine... I've just a problem in mongoDao.go with methods Insert and Update where the compiler forces me to cast input entity to a specific type ( *models.User ), but this prevents me from having a generic DAO component that works for all types. How do I fix this issue?

This generalization

DAO interface {
    Insert(entity interface{}) error

looks over-helming You both assert to *models.User for mongo

doc := entity.(*models.User)

and do

user := &models.User{}
userController.dao.Insert(user)

when use your generic DAO interface. Why don't you just define interface more precisely?

DAO interface {
    Insert(entity *models.User) error

How about creating an interface that you implement for the Entity struct?

type Entitier interface {
    GetEntity() *Entity
}

The implementation would simply return a pointer to itself that you can now use in the Insert and Update methods of your DAO. This would also have the added benefit of letting you be more specific in the declarations of your DAO methods. Instead of simply stating that they take an arbitrary interface{} as argument you could now say that they take an Entitier .

Like so:

func (dao *mongoDAO) Update(id string, update Entitier) error

Here's a minimal complete example of what I mean:

http://play.golang.org/p/lpVs_61mfM

Hope this gives you some ideas! You might want to adjust naming of Entity / Entitier / GetEntity for style and clarity once you've settled on the pattern to use.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM