Golang Project Structure

Tutorials, tips and tricks for writing and structuring code in Go (with some additional content for other programming languages)

Easy Guide to Using MongoDB With Go

Language

  • unknown

by

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.

A large pile of paper documents.
Data in MongoDB is stored in records called documents.

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.

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.

Leave a Reply

Your email address will not be published. Required fields are marked *