I don't think there's one good answer to this question, multiple approaches may be good, one better from a specific point of view, and another being better from another point of view.
Just try to create an interface (not in the meaning of Go interface type) that hides DB specific behavior and types, which will leave you the option to easily switch to a new db implementation later on, given that all other parts of your code strictly access the persistent layer via this DB interface.
The DB interface should define Go model types (modeling data stored in the persistent layer), and operations on these types, e.g. load, find, save.
As an example, modeling a user:
package db
type ID int64
type User struct {
ID ID
Name string
}
// Manager contains the operations that involve the persistence layer.
type Manager interface {
LoadUser(id ID) (*User, error)
SaveUser(u *User) error
FindUsersByName(name string) ([]*User, error)
Close() error
}
And you may create an implementation (or multiple ones) of the Manager
interface. An implementation using MongoDB:
package mongo
import (
"db"
"gopkg.in/mgo.v2"
)
// manager is a db.Manager implementation that uses MongoDB
type manager struct {
// unexported fields, e.g. MongoDB session:
sess *mgo.Sess
}
func (m *manager) LoadUser(id db.ID) (*db.User, error) { ... }
func (m *manager) SaveUser(u *db.User) error { ... }
func (m *manager) FindUsersByName(name string) ([]*db.User, error) { ... }
func (m *manager) Close() error {
m.sess.Close()
return nil
}
func New(mongoURL string) (db.Manager, error) {
// Create, initialize your manager, and return it:
sess, err := mgo.Dial(url)
if err != nil {
return nil, err
}
return &manager{sess: sess}, nil
}
A db.Manager
instance (since it involves building a connection to a (Mongo)DB server) should be kept for as long as possible (e.g. global instance). Depending on the usage, Manager.Copy()
and Manager.Clone()
operations should be supported to acquire a copy or clone for short lived usage (e.g. serving an HTTP request).
Using this example: Someone somewhere has to call mongo.New()
to acquire a value of the db.Manager
, but from there we only have to interact with the persistence layer via the Manager
, leaving any db specific detail to the implementation.
For example:
var mgr db.Manager
var err error
mgr, err = mongo.New("<mongodburl>")
if err != nil {
log.Printf("Could not connect to db:", err)
return
}
defer mgr.Close()
id := 123456
u, err := mgr.LoadUser(id)
if err != nil {
log.Printf("Failed to load user [id: %v]: %v
", id, err)
return
}
fmt.Printf("Loaded User: %+v
", u)