The connection that doesn't connect
You are building a service that needs a document store. MongoDB is the choice because the schema is flexible and the query language is expressive. You install the official driver, write a few lines to connect, and run the program. It prints "Connected" and exits. You feel good.
Then you deploy to a load test. The service hangs after a few minutes. Or worse, it burns through all available file descriptors and crashes. The issue isn't the database. The issue is how the driver manages connections, contexts, and resources. The official driver is powerful, but it requires explicit lifecycle management. There is no magic auto-close. There is no global connection state. You control the client, and you control the context.
The official driver is go.mongodb.org/mongo-driver. It wraps the MongoDB wire protocol and gives you a type-safe Go API. It returns errors, never panics. It uses context.Context for every operation. It manages a connection pool internally. Understanding these patterns prevents the most common production bugs.
How the driver manages resources
The driver centers around *mongo.Client. This is not a single TCP connection. It is a handle to a connection pool. When you create a client, the driver prepares to talk to the server. It does not open connections immediately. Connections are established lazily when you perform an operation like a query or an insert.
The client is thread-safe. Multiple goroutines can use the same client concurrently. This is by design. You create one client at application startup and pass it to every handler, worker, and service layer. Creating a new client for every request is a resource leak. Each client maintains its own pool, and the OS has limits on open sockets and memory.
Every operation takes a context.Context as the first argument. The context controls cancellation and deadlines. If the context expires, the driver aborts the operation and returns an error. If you forget to cancel a context, the driver keeps resources alive longer than necessary. The context is the leash. Pull it to stop the work.
Convention aside: context.Context always goes as the first parameter, conventionally named ctx. Functions that accept a context should respect cancellation and deadlines. The driver checks the context before sending data and while waiting for a response.
Minimal connection example
Here is the simplest way to connect, verify the server, and clean up. This pattern appears in almost every Go program that uses MongoDB.
package main
import (
"context"
"log"
"time"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
func main() {
// Timeout prevents the program from hanging forever if the server is unreachable.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// ApplyURI parses the connection string and configures the client options.
client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017"))
if err != nil {
log.Fatal(err)
}
// Disconnect releases the connection pool and network resources.
// Always defer this after a successful connect to avoid leaks.
defer client.Disconnect(ctx)
// Ping triggers the actual handshake. Connect is lazy; Ping forces a check.
if err = client.Ping(ctx, nil); err != nil {
log.Fatal(err)
}
log.Println("Connected to MongoDB")
}
Notice the structure. context.WithTimeout creates a deadline. defer cancel ensures the timeout is cleaned up even if the function returns early. mongo.Connect returns a client and an error. If the error is non-nil, the client is unusable. defer client.Disconnect runs when main exits. Ping verifies the server is alive.
Convention aside: if err != nil { return err } is verbose by design. The community accepts the boilerplate because it makes the unhappy path visible. The driver returns errors for every failure mode. Check them.
Working with documents and structs
MongoDB stores documents as BSON, which is Binary JSON. Go maps structs to BSON using reflection. The driver reads struct fields and tags to serialize data. Fields must be exported (capitalized) for the driver to see them. The bson tag controls the field name in the database.
Here is a realistic example that defines a struct, inserts a document, and retrieves it.
type Event struct {
// ID is generated by MongoDB if omitted. The omitempty tag prevents sending an empty string.
ID string `bson:"_id,omitempty"`
// Type classifies the event.
Type string `bson:"type"`
// Payload holds arbitrary data.
Payload map[string]interface{} `bson:"payload"`
}
func storeAndRetrieve(ctx context.Context, client *mongo.Client) error {
// Collection returns a handle to the events collection in the app database.
col := client.Database("app").Collection("events")
// InsertOne sends the document to the server. It returns the inserted ID.
result, err := col.InsertOne(ctx, Event{
Type: "signup",
Payload: map[string]interface{}{"user": "alice"},
})
if err != nil {
return err
}
// InsertedID is an interface{} that can be cast to ObjectId or string.
log.Printf("Inserted event with ID: %v", result.InsertedID)
// FindOne retrieves a single document. Decode populates the struct.
var found Event
err = col.FindOne(ctx, bson.M{"type": "signup"}).Decode(&found)
if err != nil {
return err
}
log.Printf("Found event: %+v", found)
return nil
}
The bson:"_id,omitempty" tag is important. If you omit omitempty, the driver sends an empty string for ID, which might conflict with MongoDB's auto-generated ObjectId. The omitempty tag tells the driver to skip the field if it has the zero value.
InsertOne returns a *mongo.InsertOneResult. The InsertedID field contains the ID used by the server. If you didn't provide an ID, MongoDB generates one. The type of InsertedID depends on what you sent or what the server generated. It is often a primitive.ObjectID.
Convention aside: Public names start with a capital letter. Private names start lowercase. The driver uses reflection, so struct fields must be public. Tags are lowercase. No keywords like public or private.
BSON: maps, slices, and order
Go maps do not preserve insertion order. BSON documents are ordered. This matters when you use update operators or create indexes. The driver provides two types for building documents: bson.M and bson.D.
bson.M is a map. It is convenient for queries where order doesn't matter. bson.D is a slice of key-value pairs. It preserves order. Use bson.D when the server cares about order, such as in update pipelines or when you need to ensure a specific field appears first.
The compiler rejects code that passes the wrong type. If you pass a struct where a map is expected, you get cannot use struct as map value in argument. If you forget to import bson, you get undefined: bson. The driver types are strict.
When you build an update, order can affect performance or correctness. For example, a $set followed by a $unset might behave differently than the reverse if the fields overlap. bson.D guarantees the order you write.
import "go.mongodb.org/mongo-driver/bson"
// Update uses bson.D to preserve the order of operators.
// The server processes operators in the order they appear.
update := bson.D{
{"$set", bson.M{"status": "processed"}},
{"$unset", bson.M{"temp_flag": ""}},
}
// UpdateOne applies the update to the first matching document.
res, err := col.UpdateOne(ctx, bson.M{"_id": id}, update)
if err != nil {
return err
}
Convention aside: Don't pass a *string. Strings are already cheap to pass by value. The driver accepts values, not pointers to primitives. Pointers are useful for optional fields where nil means "not set", but for simple updates, values are clearer.
Pitfalls and errors
The driver returns standard Go errors. You can check errors using errors.Is and errors.As. The driver provides helper functions for common MongoDB errors. For example, mongo.IsDuplicateKeyError(err) checks if the error is a duplicate key violation.
Goroutine leaks happen when the goroutine waits on a channel that never gets closed. In the driver, leaks happen when you forget to cancel a context or disconnect the client. Always have a cancellation path. If a goroutine calls Find with a context that never expires, the goroutine blocks until the query returns or the server drops the connection.
The driver manages a connection pool. If you create too many clients, you exhaust the pool. The default pool size is 100 connections. You can adjust this with options.Client().SetMaxPoolSize(n). If your application needs more concurrency, increase the pool size. If you see connection timeouts, check the pool metrics.
Compiler errors are inline and descriptive. If you pass a context with the wrong type, the compiler complains with cannot use ctx (variable of type context.Context) as string value in argument. If you miss a required argument, you get not enough arguments in call to mongo.Connect. Read the error. It tells you exactly what is wrong.
Runtime panics are rare in the driver. The driver returns errors instead. If you see a panic, it is usually a nil pointer dereference in your code, not the driver. Check that the client is not nil before calling methods. Check that the context is not nil.
Convention aside: gofmt is mandatory. Don't argue about indentation. Let the tool decide. Most editors run gofmt on save. The driver code follows standard Go formatting. Your code should too.
Decision matrix
Use mongo.Connect once at application startup to create a singleton client. Use client.Disconnect when the application shuts down to release resources. Use context.WithTimeout for every database operation to prevent hanging goroutines. Use bson.D when field order matters, such as in update operators or indexed queries. Use bson.M for simple queries where order is irrelevant. Use mongo.IsDuplicateKeyError to handle unique constraint violations explicitly. Use options.Client().SetMaxPoolSize when you need more concurrent connections than the default. Use defer cancel immediately after creating a context with a timeout or deadline. Use struct tags to map Go fields to BSON keys and control serialization. Use the driver's error helpers to distinguish between network errors, command errors, and validation errors.
The client is a resource. Treat it like a database connection pool, not a disposable object. Context is the leash. Pull it to stop the operation. BSON order matters when the server cares.