Decode value from mongodb cursor which is of interface type

Viewed 1114

I am writing an abstraction over the official mongo driver which consists of a struct containing a pointer to the collection needed and CRUD methods on it. In order to be able to work with multiple types(all of which have bson adnotations) I use an interface called Storable, but I don't see a way in which I could decode the field without knowing the type exactly. Code snippet:

func (c *Collection) GetAll() ([]models.Storable, error) {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    cursor, err := c.coll.Find(ctx, bson.M{})
    if err != nil {
        return nil, err
    }

    var result []models.Storable

    for cursor.Next(ctx) {
        var doc models.Storable
        err = cursor.Decode(&doc)
        if err != nil {
            return nil, err
        }
        result = append(result, doc)
    }
    return result, nil
}

type example:

type User struct {
    ID        primitive.ObjectID `json:"id" bson:"_id,omitempty"`
    FirstName string             `json:"firstName" bson:"firstName"`
    LastName  string             `json:"lastName" bson:"lastName"`
    Email     string             `json:"email" bson:"email"`
    Password  string             `json:"password" bson:"password"`
}
1 Answers

You need to pass in the correct type to decode to somehow. To be able to pass in different types you could use the empty interface{}. However if you pass in the entire slice (e.g. []User) into interface{} you cannot append to it any more without complex usage of reflection.

Using a function to create a new row

As @mkopriva mentioned in the comments, we could pass a function creating a new row to it (or pass it to the collection on initialisation):

func (c *Collection) GetAll(newRow func() models.Storable) ([]models.Storable, error) {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    cursor, err := c.coll.Find(ctx, bson.M{})
    if err != nil {
        return nil, err
    }

    var result []models.Storable

    for cursor.Next(ctx) {
        nRow := newRow()

        err = cursor.Decode(nRow)
        if err != nil {
            return nil, err
        }
        result = append(result, nRow.(models.Storable))
    }
    return result, nil
}

You would then call with a function returning the correct type (must be a pointer):

rows, err := c.GetAll(func() models.Storable {
    return new(User)
})

Here my reference implementation using json decoding to test this: Playground

Using reflection to create a new row

You could pass in the variable type for a single row:

func (c *Collection) GetAll(row models.Storable) ([]models.Storable, error) {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    cursor, err := c.coll.Find(ctx, bson.M{})
    if err != nil {
        return nil, err
    }

    var result []models.Storable

    for cursor.Next(ctx) {
        nRow := reflect.New(reflect.TypeOf(p)).Interface()

        err = cursor.Decode(nRow)
        if err != nil {
            return nil, err
        }
        result = append(result, nRow.(models.Storable))
    }
    return result, nil
}

You would then call this with the correct type:

var user User
rows, err := c.GetAll(user)

Note that I don't use a pointer type here! The pointer is taken via reflection in the function.

Here my reference implementation using json decoding to test this: Playground


The downside of the "one function for all" is that you now have a slice of models.Storable with different data inside and you have to use a type switch or type assertion to work with the data.

For this reason I don't use generic functions for database calls but create a new function for every call I need: e.g. GetAllUsers, GetUserByID, GetAllContacts, etc.

Note: this is one of the things I will rethink when generics get added to Go.

Related