Easy Guide to Using MongoDB With Go
Language
- unknown
by James Smith (Golang Project Structure Admin)
MongoDB is a popular NoSQL database, known for its flexibility and scalability.
Its flexible schema allows developers to work efficiently with different data types, making it well-suited for dynamic and rapidly evolving applications.
MongoDB stores data in JSON-like format called BSON (Binary JSON), which makes it a great choice for handling large-scale and relatively unstructured data.
As many readers of this blog will know, Go is a statically typed, compiled programming language developed by Google. It is often praised for its simplicity and efficiency.
Combining MongoDB and Go can result in powerful applications that leverage the impressive performance of Go and the versatile flexibility of MongoDB’s document model.
In this blog post, we’ll work step-by-step as we look at how to set up MongoDB and perform some basic CRUD operations — i.e. Create, Read, Update and Delete.
Then we’ll look at how to handle more advanced topics such as indexing, aggregation and transactions.
Table of Contents
How to Install MongoDB
I shall assume that you already have the Go programming language installed on your machine, but we will look at how to install the MongoDB database.
MongoDB supports multiple platforms, and the installation process varies slightly depending on your operating system.
Below are the instructions for installing MongoDB on all of the major operating systems.
Installing MongoDB on macOS
If you don’t have Homebrew installed, you can install it by running the following command in your terminal:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Once Homebrew is installed, you can then use it to install MongoDB:
brew tap mongodb/brew
brew install mongodb-community@6.0
This will install the latest stable version of MongoDB (version 7.0 at the time of writing).
You can start MongoDB as a service using Homebrew’s services
command:
brew services start mongodb/brew/mongodb-community
This will automatically start the MongoDB server running in the background. You can stop it using:
brew services stop mongodb/brew/mongodb-community
To ensure that MongoDB is running correctly, you can connect to the MongoDB shell:
mongo
This will open the MongoDB shell, and you should see a prompt like >
, where you can run MongoDB commands.
Installing MongoDB on Windows
Go to the MongoDB Download Center and choose the Windows installer (which should be a .msi file) for the latest MongoDB Community Server version.
Once downloaded, run the installer. When prompted, select the Complete installation option, which will install all of the necessary components.
In the installation options, ensure you check the box to install MongoDB as a Windows service. This allows MongoDB to start automatically when your computer starts.
Once installed, MongoDB should start automatically as a service. You can verify this by opening the Windows Services app (services.msc) and looking for the MongoDB service.
By default, MongoDB stores its data in C:\Program Files\MongoDB\Server\{version}\bin\data
. If the folder doesn’t exist, create it manually.
Now open a Command Prompt and run:
mongo
This will open the MongoDB shell, and you can interact with MongoDB from there.
Installing MongoDB on Linux
For most Linux distributions, MongoDB provides an official repository.
Below are steps for installing MongoDB on Ubuntu. Other distributions have similar procedures.
Open a terminal and import the public GPG key used by the MongoDB package management system:
wget -qO - https://www.mongodb.org/static/pgp/server-6.0.asc | sudo apt-key add -
Create a file for MongoDB in the /etc/apt/sources.list.d/
directory:
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/6.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-6.0.list
After adding the MongoDB repository, update your local package database:
sudo apt-get update
Install MongoDB with the following command:
sudo apt-get install -y mongodb-org
Once installed, start MongoDB with:
sudo systemctl start mongod
To have MongoDB automatically start on boot:
sudo systemctl enable mongod
Run the following command to check MongoDB’s status:
sudo systemctl status mongod
Fianlly, you can also start the MongoDB shell to verify everything is working:
mongo
Using MongoDB with Docker
If you prefer using Docker for a containerized setup, then you can quickly spin up a MongoDB instance with Docker.
Ensure you have Docker installed on your machine. Follow the installation guide from Docker’s official site.
Then use the following command to run MongoDB in a Docker Container:
docker run --name mongodb -d -p 27017:27017 mongo
This command will pull the official MongoDB Docker image and run it, exposing port 27017 (MongoDB’s default port) to your local machine.
After running that command, check if MongoDB is running by connecting to the MongoDB shell from within the container:
docker exec -it mongodb mongo
Connecting to MongoDB From Go Code
The first step in interacting with MongoDB is to establish a connection to the database.
The following code demonstrates how to connect to a local MongoDB server using the Go MongoDB driver:
package main
import (
"context"
"fmt"
"log"
"time"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
clientOptions := options.Client().ApplyURI("mongodb://localhost:27017")
client, err := mongo.Connect(ctx, clientOptions)
if err != nil {
log.Fatal(err)
}
err = client.Ping(ctx, nil)
if err != nil {
log.Fatal("Failed to connect to MongoDB:", err)
}
fmt.Println("Connected to MongoDB!")
}
To ensure reliable communication with MongoDB, we go through several steps when setting up the connection in Go.
First we use the context.WithTimeout
function from Go’s standard library to set up a timeout for the connection.
This is crucial because it prevents the operation from hanging indefinitely if MongoDB is slow to respond.
In this case, we’ve configured the timeout to cancel the operation if no response is received within ten seconds, ensuring that the application remains responsive even under poor network conditions.
Next we configure the client options using the ApplyURI
function on the result of the options.Client
function. This specifies the MongoDB URI that the client should connect to.
In most development environments, MongoDB is hosted locally, so we will typically want to connect to localhost:27017
, which is the default address for a local MongoDB instance.
Once the client options are set, the mongo.Connect
function is called. This initiates the connection to MongoDB based on the provided options.
It’s at this stage that the application actually attempts to establish communication with the database.
Finally, in order to verify that the connection has been successfully established, we use the client.Ping
function. This sends a simple ping request to the MongoDB server.
If MongoDB responds, we know that the connection is active and ready for queries. However, if it fails, this indicates an issue with the connection that needs to be addressed.
If you’re using MongoDB Atlas (which is is a cloud-based service that lets you easily set up, manage and scale MongoDB databases without needing to handle the underlying infrastructure) or a different host, then remember update the connection URI accordingly.
For example, you could have initialized the client options like so:
clientOptions := options.Client().ApplyURI("mongodb+srv://:@cluster0.mongodb.net/exampleDatabase?retryWrites=true&w=majority")
Closing the MongoDB Connection in Go
Of course, it’s important to close the connection when you’re done using the MongoDB client, in order to release resources.
This can be done like so:
defer func() {
if err := client.Disconnect(ctx); err != nil {
log.Fatal(err)
}
}()
The use of the defer
keyword ensures that the connection will be closed whenever the surrounding function comes to an end.
Working With MongoDB Collections
Once we’ve established a reliable connection, the next step is learning how to interact with collections.
Collections in MongoDB are akin to tables in relational databases, where each collection holds a set of documents. You can think of collections as being a bit like rows in MySQL.
To interact with a MongoDB collection, we use the Database
and Collection
methods from the MongoDB client.
Here’s how you can access a collection using an active MongoDB client in Go:
collection := client.Database("example_db").Collection("users")
In this case, example_db
is the name of the MongoDB database, and users
is the collection that we’ll be working with.
Inserting a Single Document into a Collection
In MongoDB, documents are stored in collections.
These documents are represented as BSON, but in Go, we use the bson.D
(which is an ordered map) or bson.M
(which is an unordered map) types to define documents.
I assume that the letter D in the bson.D
type stands for the word “dictionary”, but don’t quote me on that.
Here’s an example of inserting a single document into a MongoDB collection:
package main
import (
"context"
"fmt"
"log"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
clientOptions := options.Client().ApplyURI("mongodb://localhost:27017")
client, err := mongo.Connect(ctx, clientOptions)
if err != nil {
log.Fatal(err)
}
defer client.Disconnect(ctx)
collection := client.Database("example_db").Collection("users")
// this is the document we'll be inserting
user := bson.D{
{Key: "name", Value: "Alice"},
{Key: "age", Value: 26},
{Key: "email", Value: "alice@example.com"},
}
insertResult, err := collection.InsertOne(ctx, user)
if err != nil {
log.Fatal(err)
}
fmt.Println("Inserted document with the following ID:", insertResult.InsertedID)
}
In the example above, the document structure is represented using bson.D
, an ordered map
Each entry within this map is a bson.E
element, where the Key
represents the field name and the Value
holds the corresponding data.
This structure ensures that the order of fields in the document is maintained, which can be important for certain operations or when interacting with MongoDB’s BSON format.
As the name would suggest, the InsertOne
method is used to insert a single document into a MongoDB collection.
When called, it adds the provided document to the specified collection and returns the document’s ID.
If an ID is not provided explicitly, MongoDB automatically generates one, ensuring that each document has a unique identifier within the collection.
Inserting Multiple Documents into a Collection
The InsertOne
function that we’ve just seen is commonly used when working with individual records in MongoDB.
However, if you want to insert multiple documents at once, then you can use the InsertMany
function, as shown in the example below:
users := []interface{}{
bson.D{{Key: "name", Value: "Bob"}, {Key: "age", Value: 30}, {Key: "email", Value: "bob@example.com"}},
bson.D{{Key: "name", Value: "Charlie"}, {Key: "age", Value: 35}, {Key: "email", Value: "charlie@example.com"}},
}
insertManyResult, err := collection.InsertMany(ctx, users)
if err != nil {
log.Fatal(err)
}
fmt.Println("Inserted multiple documents with the following IDs:", insertManyResult.InsertedIDs)
This works in much the same way as adding a single document to the collection, except that the InsertMany
function takes a slice of documents to insert.
Retrieving a Single Document From a Collection
After inserting documents into a collection, now we should learn how to retrieve them.
MongoDB’s FindOne
method allows you to search for a document from a collection.
Here’s how to retrieve a single document:
var result bson.M
filter := bson.D{{Key: "name", Value: "Alice"}}
err = collection.FindOne(ctx, filter).Decode(&result)
if err != nil {
log.Fatal(err)
}
fmt.Println("Found document:", result)
The FindOne
method is used to retrieve a single document from a MongoDB collection that matches the provided filter criteria.
Even if multiple documents meet the filter’s conditions, only the first matching document is returned by this method.
This is useful when you’re looking for a specific entry or just need the first result from a query.
Once the document is retrieved, the Decode
function is called to unmarshal the BSON document into a Go variable.
In this case, the result is stored in a bson.M
type, an unordered map in Go. This map structure allows for flexible storage of the document’s key-value pairs, making it easier to work with the retrieved data in Go applications.
Retrieving Multiple Documents From a Collection
To retrieve multiple documents, you can use the Find
method, as shown in the example below:
filter := bson.D{{Key: "age", Value: bson.D{{Key: "$gt", Value: 20}}}}
cursor, err := collection.Find(ctx, filter)
if err != nil {
log.Fatal(err)
}
defer cursor.Close(ctx)
for cursor.Next(ctx) {
var user bson.M
if err := cursor.Decode(&user); err != nil {
log.Fatal(err)
}
fmt.Println(user)
}
if err := cursor.Err(); err != nil {
log.Fatal(err)
}
The filter in the example above is designed to retrieve all documents from the MongoDB collection where the age
field is greater than 20.
MongoDB supports a range of operators for querying, and, in this case, "$gt"
is used to denote “greater than”.
Other common MongoDB query operators include "$lt"
for “less than” and "$eq"
for “equals”. These operators allow for flexible yet precise searches within the database.
The Find
method is then called, returning a cursor object. A cursor in MongoDB allows for the efficient retrieval of multiple documents from a query result.
Instead of fetching all documents at once, the cursor provides a way to iterate through the result set, handling large volumes of data with minimal memory usage.
The cursor.Next
method is used to advance the cursor to the next document in the result set. This loop continues until all matching documents have been retrieved.
For each document, the Decode
function is used to unmarshal the BSON data into the user
variable, which is a bson.M
map type in Go.
This ensures that you can easily access and work with each document’s fields during the iteration process.
Updating a Single Document Within a Collection
MongoDB supports various update operations.
For example, the UpdateOne
method allows us to update a single document:
filter := bson.D{{Key: "name", Value: "Alice"}}
update := bson.D{{Key: "$set", Value: bson.D{{Key: "age", Value: 26}}}}
updateResult, err := collection.UpdateOne(ctx, filter, update)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Matched %v documents and updated %v documents.\n", updateResult.MatchedCount, updateResult.ModifiedCount)
The filter in the code above specifies that the update operation should target any documents where the name
field is "Alice"
.
This filter is similar to the one used for querying documents, allowing MongoDB to identify the specific documents that should be updated.
The update statement is defined using the "$set"
operator, which is commonly used in MongoDB to modify fields in a document.
Here, the update specifies that the age
field for Alice should be set to 26.
The "$set"
operator ensures that only the age
field is updated, without affecting any other fields in the document.
After performing the update with the UpdateOne
function, the result contains two important fields: MatchedCount
and ModifiedCount
.
MatchedCount
indicates how many documents matched the filter — in this case, how many documents with the name "Alice"
were found.
With the UpdateOne
method, the MatchedCount
should be either one or zero.
ModifiedCount
reflects how many of those matched documents were actually modified, based on whether the update changed any values.
Updating Multiple Documents Within a Collection
In addition to updating a single document, MongoDB provides the UpdateMany
method, which allows us to update multiple documents that match a specified filter.
Here’s how it’s used:
filter := bson.D{{Key: "status", Value: "pending"}}
update := bson.D{{Key: "$set", Value: bson.D{{Key: "status", Value: "completed"}}}}
updateResult, err := collection.UpdateMany(ctx, filter, update)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Matched %v documents and updated %v documents.\n", updateResult.MatchedCount, updateResult.ModifiedCount)
In the example above, the filter specifies that we want to update all documents with a status
field set to "pending"
.
Every document that matches this criteria will have its status
updated to "completed"
.
Similar to UpdateOne
, the UpdateMany
method also returns a result that contains MatchedCount
and ModifiedCount
.
Using UpdateMany
is particularly useful when you want to apply the same update to multiple documents in your collection without needing to loop through each document individually. This can lead to more efficient database operations and cleaner code.
Remember that if no documents match the filter, both MatchedCount
and ModifiedCount
will be zero, indicating that no changes were made.
Deleting a Single Document Within a Collection
To delete just one document, you can use the DeleteOne
method:
filter := bson.D{{Key: "name", Value: "Alice"}}
deleteResult, err := collection.DeleteOne(ctx, filter)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Deleted %v document[s].\n", deleteResult.DeletedCount)
The filter defined above ensures that the operation will target any document where the name
field is equal to "Alice"
.
This filter allows MongoDB to identify the specific document to delete.
The DeleteOne
method removes only the first document that matches the given filter criteria. In this case, it searches for a document with the name "Alice"
and deletes that record from the collection.
If multiple documents share the same name, only the first match will be deleted.
Once the operation is executed, the deleteResult
object contains a field called DeletedCount
, which reflects how many documents were deleted.
In this case, the value will indicate whether the delete operation successfully removed a document that matched the filter.
Deleting Multiple Documents From a Collection
To remove multiple documents that match a specific filter, you can use the DeleteMany
method.
This is particularly useful when you want to clean up a collection by removing all documents that meet certain criteria.
Here’s an example of how to use the DeleteMany
method:
filter := bson.D{{Key: "age", Value: bson.D{{Key: "$gt", Value: 30}}}}
deleteResult, err := collection.DeleteMany(ctx, filter)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Deleted %v documents.\n", deleteResult.DeletedCount)
The filter specifies that any document with an age
field greater than 30 should be deleted.
The DeleteMany
method will execute the deletion operation for all documents matching the filter.
After the operation, the deleteResult
object contains the DeletedCount
field, which indicates how many documents were successfully deleted from the collection.
If no documents have matched the filter criteria, then DeletedCount
will be zero, signifying that there were no documents to delete.
This method is efficient for batch deletions, ensuring you can manage your MongoDB collections effectively without the need for an iterative deletion processes.
Setting up Indexing
Indexes (or “indices”) are useful for optimizing the performance of queries in MongoDB.
They allow MongoDB to quickly find documents that match query criteria.
To create an index in Go, use the CreateOne
method chained on the collection.Indexes
method, as shown below:
indexModel := mongo.IndexModel{
Keys: bson.D{{Key: "email", Value: 1}},
Options: nil,
}
indexName, err := collection.Indexes().CreateOne(ctx, indexModel)
if err != nil {
log.Fatal(err)
}
fmt.Println("Created index:", indexName)
In the example above, we are creating an index on the email
field of a MongoDB collection.
Indexes improve query performance by allowing MongoDB to quickly locate documents that match specific criteria.
The Keys
field in the IndexModel
defines the field to be indexed.
In this case, we specify that the email
field should be indexed in ascending order.
The value 1
denotes an ascending index, while a value of -1
would indicate a descending index.
Indexing a field can significantly improve search speed when querying by that field, especially in large collections.
The CreateOne
method actually does the work of creating the index in MongoDB.
This method takes the context and the index model as parameters and creates the index based on the specified fields and options.
It returns the name of the newly created index, which is stored here in the indexName
variable.
Performing Aggregation
MongoDB’s aggregation framework allows us to perform complex data processing — such as grouping, filtering and transforming documents.
Here’s an example that calculates the average age of all the users in our collection:
pipeline := mongo.Pipeline{
{{Key: "$group", Value: bson.D{{Key: "_id", Value: nil}, {Key: "averageAge", Value: bson.D{{Key: "$avg", Value: "$age"}}}}}},
}
cursor, err := collection.Aggregate(ctx, pipeline)
if err != nil {
log.Fatal(err)
}
defer cursor.Close(ctx)
for cursor.Next(ctx) {
var result bson.M
if err := cursor.Decode(&result); err != nil {
log.Fatal(err)
}
fmt.Println(result)
}
The aggregation is performed using a pipeline, which is an array of stages that transform and analyze data in sequence.
The pipeline starts with the "$group"
stage, which is used to group documents together and perform operations on them.
Here the "$group"
stage groups all documents into a single group, as indicated by {Key: "_id", Value: nil}
, which means we aren’t grouping by any specific field.
Within this group, the averageAge
field is calculated using the "$avg"
operator, which computes the average value of the age
field across all documents.
The Aggregate
method is then called to apply the pipeline to the MongoDB collection.
This method takes the context and the pipeline as arguments, and it returns a cursor to iterate over the results.
The cursor.Next
function is used to advance through the results of the aggregation.
For each result, the Decode
function is called to unmarshal the BSON document into a Go bson.M
map, as we saw in a previous example.
This allows the program to access and print the result, which in this case will include the calculated average age of the users in the collection.
The program also ensures that the cursor is properly closed after use by calling cursor.Close
when the function comes to an end with the use of the defer
keyword.
Using Transactions
MongoDB allows you to perform multi-document transactions, meaning you can group multiple operations into a single, atomic transaction.
This feature works in both replica set environments, which are used for redundancy and high availability, and sharded cluster environments, where data is distributed across multiple servers for scalability.
This capability ensures that either all of the operations succeed together, or none of them do, helping to maintain data integrity and consistency even in complex database setups.
So all of the operations within a transaction will either complete successfully or roll back.
Here’s an example of using a transaction in Go:
session, err := client.StartSession()
if err != nil {
log.Fatal(err)
}
defer session.EndSession(ctx)
callback := func(sessCtx mongo.SessionContext) (interface{}, error) {
_, err := collection.InsertOne(sessCtx, bson.D{{Key: "name", Value: "Eve"}, {Key: "age", Value: 28}})
if err != nil {
return nil, err
}
_, err = collection.UpdateOne(sessCtx, bson.D{{Key: "name", Value: "Bob"}}, bson.D{{Key: "$set", Value: bson.D{{Key: "age", Value: 31}}}})
if err != nil {
return nil, err
}
return nil, nil
}
_, err = session.WithTransaction(ctx, callback)
if err != nil {
log.Fatal(err)
}
fmt.Println("Transaction complete")
The code starts by creating a session using the StartSession
method.
This session is necessary to run a transaction in MongoDB, as it maintains the state and context of the transaction.
If any error occurs while starting the session, it is logged, and the program exits.
To ensure proper cleanup, the session is ended using by using the defer
keyword to call the session.EndSession
method after the transaction is complete. It is assumed that the code snippet above would be placed inside its own function.
Then a callback function is defined. The callback takes a SessionContext
argument, which is a context that is aware of the ongoing session and ensures that all database operations within the callback are executed as part of the same transaction.
In this case, the transaction performs two operations: first, it inserts a new document for a user named "Eve"
with an age of 28 using InsertOne
, and, second, it updates the age of the user "Bob"
to 31 using UpdateOne
.
If any of these operations fail, the error is returned and the entire transaction is rolled back.
Finally, the WithTransaction
method is used to run the transaction, passing the session context and the callback function as arguments.
If any error occurs during the transaction, all the operations are rolled back, meaning that no changes will be made to the database.
However, if everything succeeds, the transaction is committed — and the changes will be permanently saved.
This code ensures atomicity, which means that either all operations are applied or none. This is critical for maintaining data integrity when multiple changes are involved in a sequence of operations.