MongoDB and Go - Moving on from mgo?

Published

The state of MongoDB drivers for Go is in flux. In this article, we update out Grand Tour for the current mgo and in-development MongoDB drivers and give a feel for the future of Go and MongoDB.

The state of mgo

In January this year the developer of mgo, the de facto Go package for working with MongoDB, announced that he was no longer developing the package. Gustavo Niemeyer explained that he'd moved on to using other databases.

This was not a surprise; the mgo package had lay without significant updates and an expanding queue of pull requests for around two years before that announcement and users had begun forking it to create a community maintained branch, globalsign/mgo.

Here at Compose, we've just updated our Grand Tour example to use the community supported version and we're updating our general guidance on available drivers for Go and MongoDB to point people at that community supported version. First up, there's an updated version of the mgo based version of the Grand Tour example. You can find it at [https://github.com/compose-grandtour/golang/blob/master/example-mongodb/example-mongodb.go

Post mgo - A new driver appears

In the same month as Niemeyer announced the unmaintained status of mgo, MongoDB Inc announced it was developing its own Go package.

MongoDB Inc has used Go extensively. In particular, it based its rewrite of the MongoDB tools on Go and using mgo. It maintained its own internal fork with patches it felt it needed and opened up mgo abstractions where it needed to. When the time came to decide what to do about Go support, MongoDB went with the ambitious plan of writing their own driver from scratch.

The new MongoDB Go driver is currently in alpha; alpha 6 at time of writing. Design-wise, it aims to conform with MongoDB's other drivers with similar abstractions and encapsulations, along with similar short-circuits for performance and reliability through those abstractions.

We had one big question - what does it look like in use? We set out to port the Grand Tour example to the new driver to get a feel for it. The complete code for this new example is available in the mongodb-go-driver directory of the Go Grand Tour.

Grand Retour - Connecting with MongoDB-go-driver

Let's start with making the connection. With mgo, a non-TLS/SSL connection was a breeze to create, but as soon as TLS/SSL entered the connection equation, you find yourself creating certificate pools and tls.Config settings, editing the connection string to remove unsupported arguments and creating special dial functions. With the MongoDB-go-driver, it's quite a bit simpler:

connectionString, present := os.LookupEnv("COMPOSE_MONGODB_URL")  
if !present {  
    log.Fatal("Need to set COMPOSE_MONGODB_URL environment variable")
}

certpath, certavail := os.LookupEnv("PATH_TO_MONGODB_CERT")

var err error

if certavail {  
    client, err = mongo.NewClientWithOptions(connectionString, mongo.ClientOpt.SSLCaFile(certpath))
} else {
    client, err = mongo.NewClient(connectionString)
}

if err != nil {  
    log.Fatal(err)
}

err = client.Connect(context.Background())

if err != nil {  
    log.Fatal(err)
}

defer client.Disconnect(nil)  

We open by getting the connection string and path to the self-signed certificate file. If there's no certificate, we can just do mongo.NewClient(connectionString) to create our client. Otherwise, we have mongo.NewClientWithOptions() to create the client; it takes the connection string and a ClientOptions type. This is constructed with a builder in the package and mongo.ClientOpt... starting a new set of client options. You can chain function calls to this to add settings. In this case, we just need to use .SSLCaFile(certpath), with the file name and path of the self-signed certificate. There's a whole family of ClientOptions which can be set, but for the example, we just need this one setting.

All we need to do is ask the client to connect with client.Connect(). Notice that this command takes a context as a parameter. Many of the new MongoDB driver calls support taking a context to allow for timeouts and deadlines on calls. Here, we pass context.Background() as a no-op; you can also pass nil to get a context.Background() automatically. Some though, like cursor.Close() don't and will throw an panic; the simple fix being just put an explicit context.Background() in instead.

Reading with the Mongodb-go-driver

Now we can move on to the reading of our word data. We're going to read all the word/definition entries available in the collection, and we'll sort them in this example by word. First, we need to get a handle on the collection:

c := client.Database("grand_tour").Collection("words")  

Now we need to define our sort field and order. Sorting is an option for the Find() function and that means another builder.

sort, err := mongo.Opt.Sort(bson.NewDocument(bson.EC.Int32("word", 1)))

if err != nil {  
    log.Fatal("Sort error ", err)
}

This builder is the more general Options builder. It handles options for query operations. The Sort() function takes a BSON document which in this case would effectively be { "word": 1 } but comes out as the much more wordier bson.NewDocument(bson.EC.Int32("word", 1)). This is an explicitly constructed BSON document using the bson package that comes with the driver and allows for very specifically typed documents to be created. We finally check for an error creating this option, exiting if there is one as that'd be really unexpected in this example.

Now we can do the Find(). With mgo, the find function could decode the results directly into an array of items for us. With the MongoDB driver, it's about getting a cursor that points to our results first:

cur, err := c.Find(nil, nil, sort)

if err != nil {  
    http.Error(w, err.Error(), http.StatusInternalServerError)
    return
}

defer cur.Close(context.Background())  

The function takes a content, a filter and options. Here, we're passing nil for the default context, nil for the filter as we want everything and our sort option as the options. It will return to us a cursor which we'll have to walk over. If a query function returns a single document, like FindOne, you get a DocumentResult containing the document rather than a cursor. Again, we check for an error, firing off a HTTP error if one occurs here. Finally, we defer closing the cursor till we are done.

We now have to process the cursor's results:

var items []item

for cur.Next(nil) {  
    item := item{}
    err := cur.Decode(&item)
    if err != nil {
        log.Fatal("Decode error ", err)
    }
    items = append(items, item)
}

We create an array for our results; an array of item. Then we step into a loop calling for the next item from the cursor's results. For each result, we create a new item, decode the results into the newly created item and append it to our array. It's all fairly straightforward until we come to the end.

At the end of the loop, the cursor may have thrown an error in Next() rather than come to the end naturally. How can we tell? The cursor has a Err() function which will return the last error thrown so:

if err := cur.Err(); err != nil {  
    log.Fatal("Cursor error ", err)
}

With that caught, the example goes on to re-marshal the results as JSON and send them to the requesting client. So that's reading covered, lets move on to the writing.

Writing with the MongoDB-go-driver

Writing a single document to the collection is not a convoluted process. We get a handle on our collection, we create a new item which contains the document values and then insert that new item with the InsertOne() function.

c := client.Database("grand_tour").Collection("words")  
newItem := item{ID: objectid.New(), Word: r.Form.Get("word"), Definition: r.Form.Get("definition")}  
_, err := c.InsertOne(nil, newItem)  
if err != nil {  
    log.Println(err)
    http.Error(w, err.Error(), http.StatusInternalServerError)
    return
}

One difference we found between this and the mgo version is that we apparently need to set the item's _id value using objectid.New(). With mgo, omitting it was enough for the server to generate an id. We suspect this is down to how the Go struct was serialized. To confirm that, we rewrote the code to insert with a BSON document:

newItemDoc := bson.NewDocument(bson.EC.String("word", r.Form.Get("word")), bson.EC.String("definition", r.Form.Get("definition")))  
_, err := c.InsertOne(nil, newItemDoc)  

This is, to it's credit, more explicit. If you are wondering what the id is, that's what is returned by the InsertOne() function.

Summary

The Alpha 6 version of the MongoDB Go Driver is coming along well. Initial impressions are it is much more BSON-centric in terms of parameters for filters, queries and options compared to the mgo driver. Some Go idioms parked, at least for now, in favor of equality with the other languages official MongoDB drivers. You can find the complete version of the example at https://github.com/compose-grandtour/golang/ and we'll be updating it as needed.

That said, we're looking forward to the beta release and in the meantime, we still have the mgo driver and its community to sustain MongoDB and Go developers. We're planning on keeping both examples in the Grand Tour for Go as both drivers are going to be viable development options for some time.


Read more articles about Compose databases - use our Curated Collections Guide for articles on each database type. If you have any feedback about this or any other Compose article, drop the Compose Articles team a line at articles@compose.com. We're happy to hear from you.

attribution Alessio Lin

Dj Walker-Morgan
Dj Walker-Morgan was Compose's resident Content Curator, and has been both a developer and writer since Apples came in II flavors and Commodores had Pets. Love this article? Head over to Dj Walker-Morgan’s author page to keep reading.

Conquer the Data Layer

Spend your time developing apps, not managing databases.