MongoDB é o banco de dados NoSQL mais popular do mundo, ideal para aplicações modernas que precisam de flexibilidade e escalabilidade. Combinado com Go, você tem uma stack poderosa para lidar com dados não estruturados e semi-estruturados. Neste guia, você aprenderá tudo sobre MongoDB em Go, desde operações básicas até agregações complexas.

Por Que MongoDB com Go?

Quando Escolher MongoDB

┌─────────────────────────────────────────────────────────────────┐
│  MONGODB É IDEAL PARA:                                          │
├─────────────────────────────────────────────────────────────────┤
│  ✅ Dados flexíveis e em evolução                               │
│  ✅ Alta velocidade de escrita                                  │
│  ✅ Escalabilidade horizontal (sharding)                        │
│  ✅ Documentos JSON-like nativos                                │
│  ✅ Geolocalização e queries espaciais                          │
│  ✅ Prototipagem rápida                                         │
├─────────────────────────────────────────────────────────────────┤
│  MONGODB + GO = Type-safe + Flexibilidade                       │
└─────────────────────────────────────────────────────────────────┘

MongoDB vs PostgreSQL com Go

AspectoMongoDBPostgreSQL
EsquemaFlexível (schema-less)Rígido (schema-full)
RelacionamentosEmbutidos ou referênciasJOINs nativos
EscalabilidadeHorizontal (sharding)Vertical + Replicação
Casos de usoCMS, IoT, AnalyticsFinanceiro, ERP, ACID
PerformanceExcelente para readsExcelente para ACID

Configuração do Projeto

Instalação do Driver

# Driver oficial MongoDB para Go
go get go.mongodb.org/mongo-driver/v2/mongo

# BSON utilities
go get go.mongodb.org/mongo-driver/v2/bson

# Options e configs
go get go.mongodb.org/mongo-driver/v2/mongo/options

Estrutura do Projeto

mongo-project/
├── cmd/
│   └── api/
│       └── main.go
├── internal/
│   ├── config/
│   │   └── mongodb.go
│   ├── models/
│   │   └── user.go
│   ├── repository/
│   │   └── user_repository.go
│   └── service/
│       └── user_service.go
├── go.mod
└── .env

Conexão com MongoDB

Configuração da Conexão

// internal/config/mongodb.go
package config

import (
	"context"
	"fmt"
	"log"
	"os"
	"time"

	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.mongodb.org/mongo-driver/v2/mongo/options"
)

// MongoConfig armazena configurações de conexão
type MongoConfig struct {
	URI      string
	Database string
	Timeout  time.Duration
}

// NewMongoConfig cria configuração a partir de variáveis de ambiente
func NewMongoConfig() *MongoConfig {
	return &MongoConfig{
		URI:      getEnv("MONGODB_URI", "mongodb://localhost:27017"),
		Database: getEnv("MONGODB_DATABASE", "myapp"),
		Timeout:  10 * time.Second,
	}
}

// Connect estabelece conexão com MongoDB
func (cfg *MongoConfig) Connect() (*mongo.Client, error) {
	ctx, cancel := context.WithTimeout(context.Background(), cfg.Timeout)
	defer cancel()

	// Configurar opções do cliente
	clientOptions := options.Client().
		ApplyURI(cfg.URI).
		SetMaxPoolSize(100).
		SetMinPoolSize(10).
		SetMaxConnIdleTime(30 * time.Second).
		SetServerSelectionTimeout(5 * time.Second)

	// Conectar
	client, err := mongo.Connect(clientOptions)
	if err != nil {
		return nil, fmt.Errorf("falha ao conectar ao MongoDB: %w", err)
	}

	// Verificar conexão
	if err := client.Ping(ctx, nil); err != nil {
		return nil, fmt.Errorf("falha ao ping MongoDB: %w", err)
	}

	log.Println("✅ Conectado ao MongoDB")
	return client, nil
}

// GetCollection retorna uma coleção específica
func (cfg *MongoConfig) GetCollection(client *mongo.Client, name string) *mongo.Collection {
	return client.Database(cfg.Database).Collection(name)
}

// Disconnect fecha a conexão gracefully
func Disconnect(client *mongo.Client) error {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	
	if err := client.Disconnect(ctx); err != nil {
		return fmt.Errorf("falha ao desconectar: %w", err)
	}
	
	log.Println("🔌 Desconectado do MongoDB")
	return nil
}

func getEnv(key, defaultValue string) string {
	if value := os.Getenv(key); value != "" {
		return value
	}
	return defaultValue
}

String de Conexão (Connection String)

// Exemplos de URI para diferentes cenários

// Local
mongodb://localhost:27017/myapp

// Com autenticação
mongodb://user:password@localhost:27017/myapp?authSource=admin

// Replica Set
mongodb://user:password@host1:27017,host2:27017,host3:27017/myapp?replicaSet=rs0

// MongoDB Atlas (cloud)
mongodb+srv://user:password@cluster.mongodb.net/myapp?retryWrites=true&w=majority

// Com opções adicionais
mongodb://localhost:27017/myapp?maxPoolSize=100&serverSelectionTimeoutMS=5000

Modelos e Estruturas

Definindo Documentos

// internal/models/user.go
package models

import (
	"time"

	"go.mongodb.org/mongo-driver/v2/bson/primitive"
)

// User representa um usuário no MongoDB
type User struct {
	ID        primitive.ObjectID `bson:"_id,omitempty" json:"id"`
	Email     string             `bson:"email" json:"email" validate:"required,email"`
	Name      string             `bson:"name" json:"name" validate:"required,min=3,max=100"`
	Password  string             `bson:"password" json:"-"` // Não serializar
	Profile   UserProfile        `bson:"profile" json:"profile"`
	Roles     []string           `bson:"roles" json:"roles"`
	Active    bool               `bson:"active" json:"active"`
	Tags      []string           `bson:"tags,omitempty" json:"tags,omitempty"`
	Metadata  map[string]interface{} `bson:"metadata,omitempty" json:"metadata,omitempty"`
	CreatedAt time.Time          `bson:"created_at" json:"created_at"`
	UpdatedAt time.Time          `bson:"updated_at" json:"updated_at"`
}

// UserProfile dados aninhados
type UserProfile struct {
	Bio       string    `bson:"bio,omitempty" json:"bio,omitempty"`
	Avatar    string    `bson:"avatar,omitempty" json:"avatar,omitempty"`
	BirthDate time.Time `bson:"birth_date,omitempty" json:"birth_date,omitempty"`
	Location  string    `bson:"location,omitempty" json:"location,omitempty"`
	Website   string    `bson:"website,omitempty" json:"website,omitempty"`
}

// UserCreate DTO para criação
type UserCreate struct {
	Email    string      `json:"email" validate:"required,email"`
	Name     string      `json:"name" validate:"required,min=3,max=100"`
	Password string      `json:"password" validate:"required,min=8"`
	Profile  UserProfile `json:"profile,omitempty"`
	Tags     []string    `json:"tags,omitempty"`
}

// UserUpdate DTO para atualização parcial
type UserUpdate struct {
	Name     *string      `json:"name,omitempty"`
	Email    *string      `json:"email,omitempty"`
	Profile  *UserProfile `json:"profile,omitempty"`
	Roles    []string     `json:"roles,omitempty"`
	Tags     []string     `json:"tags,omitempty"`
	Active   *bool        `json:"active,omitempty"`
}

// CollectionName retorna o nome da coleção
func (User) CollectionName() string {
	return "users"
}

Trabalhando com ObjectID

// Criar novo ObjectID
id := primitive.NewObjectID()

// Converter string para ObjectID
id, err := primitive.ObjectIDFromHex("507f1f77bcf86cd799439011")
if err != nil {
    // Handle error
}

// Converter ObjectID para string
idString := id.Hex()

// Timestamp do ObjectID
timestamp := id.Timestamp()

Operações CRUD

Repository Pattern

// internal/repository/user_repository.go
package repository

import (
	"context"
	"errors"
	"fmt"
	"time"

	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/bson/primitive"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.mongodb.org/mongo-driver/v2/mongo/options"
	"project/internal/models"
)

var (
	ErrUserNotFound = errors.New("usuário não encontrado")
	ErrEmailExists  = errors.New("email já existe")
	ErrInvalidID    = errors.New("ID inválido")
)

// UserRepository interface
type UserRepository interface {
	Create(ctx context.Context, user *models.UserCreate) (*models.User, error)
	GetByID(ctx context.Context, id string) (*models.User, error)
	GetByEmail(ctx context.Context, email string) (*models.User, error)
	List(ctx context.Context, filter ListFilter) ([]*models.User, int64, error)
	Update(ctx context.Context, id string, update *models.UserUpdate) (*models.User, error)
	Delete(ctx context.Context, id string) error
	Count(ctx context.Context, filter bson.M) (int64, error)
	Exists(ctx context.Context, email string) (bool, error)
}

// userRepo implementação
type userRepo struct {
	collection *mongo.Collection
}

// NewUserRepository cria nova instância
func NewUserRepository(db *mongo.Database) UserRepository {
	return &userRepo{
		collection: db.Collection(models.User{}.CollectionName()),
	}
}

CREATE (Inserção)

// Create insere novo usuário
func (r *userRepo) Create(ctx context.Context, user *models.UserCreate) (*models.User, error) {
	now := time.Now()
	
	newUser := models.User{
		ID:        primitive.NewObjectID(),
		Email:     user.Email,
		Name:      user.Name,
		Password:  hashPassword(user.Password),
		Profile:   user.Profile,
		Roles:     []string{"user"},
		Active:    true,
		Tags:      user.Tags,
		CreatedAt: now,
		UpdatedAt: now,
	}

	// Inserir documento
	result, err := r.collection.InsertOne(ctx, newUser)
	if err != nil {
		// Verificar erro de duplicação
		if mongo.IsDuplicateKeyError(err) {
			return nil, ErrEmailExists
		}
		return nil, fmt.Errorf("falha ao criar usuário: %w", err)
	}

	// O InsertedID já é o ID do documento
	newUser.ID = result.InsertedID.(primitive.ObjectID)
	
	// Limpar senha antes de retornar
	newUser.Password = ""
	
	return &newUser, nil
}

// CreateMany insere múltiplos documentos
func (r *userRepo) CreateMany(ctx context.Context, users []*models.UserCreate) ([]string, error) {
	docs := make([]interface{}, len(users))
	now := time.Now()
	
	for i, user := range users {
		docs[i] = models.User{
			ID:        primitive.NewObjectID(),
			Email:     user.Email,
			Name:      user.Name,
			Password:  hashPassword(user.Password),
			Active:    true,
			CreatedAt: now,
			UpdatedAt: now,
		}
	}

	result, err := r.collection.InsertMany(ctx, docs)
	if err != nil {
		return nil, fmt.Errorf("falha ao criar usuários: %w", err)
	}

	// Converter IDs para strings
	ids := make([]string, len(result.InsertedIDs))
	for i, id := range result.InsertedIDs {
		ids[i] = id.(primitive.ObjectID).Hex()
	}
	
	return ids, nil
}

func hashPassword(password string) string {
	// Use bcrypt em produção!
	// import "golang.org/x/crypto/bcrypt"
	return "hashed_" + password
}

READ (Consultas)

// GetByID busca por ID
func (r *userRepo) GetByID(ctx context.Context, id string) (*models.User, error) {
	objectID, err := primitive.ObjectIDFromHex(id)
	if err != nil {
		return nil, ErrInvalidID
	}

	var user models.User
	err = r.collection.FindOne(ctx, bson.M{"_id": objectID}).Decode(&user)
	
	if err != nil {
		if errors.Is(err, mongo.ErrNoDocuments) {
			return nil, ErrUserNotFound
		}
		return nil, fmt.Errorf("falha ao buscar usuário: %w", err)
	}

	user.Password = "" // Não retornar senha
	return &user, nil
}

// GetByEmail busca por email
func (r *userRepo) GetByEmail(ctx context.Context, email string) (*models.User, error) {
	var user models.User
	err := r.collection.FindOne(ctx, bson.M{"email": email}).Decode(&user)
	
	if err != nil {
		if errors.Is(err, mongo.ErrNoDocuments) {
			return nil, ErrUserNotFound
		}
		return nil, fmt.Errorf("falha ao buscar usuário: %w", err)
	}

	return &user, nil // Retornar com senha para autenticação
}

// ListFilter filtros para listagem
type ListFilter struct {
	Active   *bool
	Roles    []string
	Tags     []string
	Search   string
	SortBy   string
	SortDesc bool
	Page     int64
	Limit    int64
}

// List retorna lista paginada
func (r *userRepo) List(ctx context.Context, filter ListFilter) ([]*models.User, int64, error) {
	// Construir filtro
	query := bson.M{}
	
	if filter.Active != nil {
		query["active"] = *filter.Active
	}
	
	if len(filter.Roles) > 0 {
		query["roles"] = bson.M{"$in": filter.Roles}
	}
	
	if len(filter.Tags) > 0 {
		query["tags"] = bson.M{"$in": filter.Tags}
	}
	
	if filter.Search != "" {
		query["$or"] = []bson.M{
			{"name": bson.M{"$regex": filter.Search, "$options": "i"}},
			{"email": bson.M{"$regex": filter.Search, "$options": "i"}},
		}
	}

	// Contar total
	total, err := r.collection.CountDocuments(ctx, query)
	if err != nil {
		return nil, 0, fmt.Errorf("falha ao contar documentos: %w", err)
	}

	// Configurar paginação
	if filter.Page < 1 {
		filter.Page = 1
	}
	if filter.Limit < 1 || filter.Limit > 100 {
		filter.Limit = 10
	}
	skip := (filter.Page - 1) * filter.Limit

	// Configurar ordenação
	sortOrder := 1 // Ascendente
	if filter.SortDesc {
		sortOrder = -1
	}
	sortField := filter.SortBy
	if sortField == "" {
		sortField = "created_at"
	}

	opts := options.Find().
		SetSkip(skip).
		SetLimit(filter.Limit).
		SetSort(bson.D{{Key: sortField, Value: sortOrder}})

	// Executar query
	cursor, err := r.collection.Find(ctx, query, opts)
	if err != nil {
		return nil, 0, fmt.Errorf("falha ao listar usuários: %w", err)
	}
	defer cursor.Close(ctx)

	var users []*models.User
	if err := cursor.All(ctx, &users); err != nil {
		return nil, 0, fmt.Errorf("falha ao decodificar resultados: %w", err)
	}

	// Limpar senhas
	for _, u := range users {
		u.Password = ""
	}

	return users, total, nil
}

// Exists verifica se email existe
func (r *userRepo) Exists(ctx context.Context, email string) (bool, error) {
	count, err := r.collection.CountDocuments(ctx, bson.M{"email": email})
	if err != nil {
		return false, err
	}
	return count > 0, nil
}

// Count retorna contagem com filtro
func (r *userRepo) Count(ctx context.Context, filter bson.M) (int64, error) {
	return r.collection.CountDocuments(ctx, filter)
}

UPDATE (Atualização)

// Update atualiza usuário parcialmente
func (r *userRepo) Update(ctx context.Context, id string, update *models.UserUpdate) (*models.User, error) {
	objectID, err := primitive.ObjectIDFromHex(id)
	if err != nil {
		return nil, ErrInvalidID
	}

	// Construir update dinâmico
	setFields := bson.M{
		"updated_at": time.Now(),
	}
	
	unsetFields := bson.M{}

	if update.Name != nil {
		setFields["name"] = *update.Name
	}
	if update.Email != nil {
		setFields["email"] = *update.Email
	}
	if update.Active != nil {
		setFields["active"] = *update.Active
	}
	if update.Profile != nil {
		setFields["profile"] = *update.Profile
	}
	if update.Roles != nil {
		setFields["roles"] = update.Roles
	}
	if update.Tags != nil {
		if len(update.Tags) == 0 {
			unsetFields["tags"] = ""
		} else {
			setFields["tags"] = update.Tags
		}
	}

	updateDoc := bson.M{"$set": setFields}
	if len(unsetFields) > 0 {
		updateDoc["$unset"] = unsetFields
	}

	opts := options.FindOneAndUpdate().
		SetReturnDocument(options.After)

	var user models.User
	err = r.collection.FindOneAndUpdate(
		ctx,
		bson.M{"_id": objectID},
		updateDoc,
		opts,
	).Decode(&user)

	if err != nil {
		if errors.Is(err, mongo.ErrNoDocuments) {
			return nil, ErrUserNotFound
		}
		if mongo.IsDuplicateKeyError(err) {
			return nil, ErrEmailExists
		}
		return nil, fmt.Errorf("falha ao atualizar usuário: %w", err)
	}

	user.Password = ""
	return &user, nil
}

// UpdateMany atualiza múltiplos documentos
func (r *userRepo) UpdateMany(ctx context.Context, filter bson.M, update bson.M) (int64, error) {
	result, err := r.collection.UpdateMany(ctx, filter, update)
	if err != nil {
		return 0, err
	}
	return result.ModifiedCount, nil
}

// Replace substitui documento completo
func (r *userRepo) Replace(ctx context.Context, id string, user *models.User) error {
	objectID, err := primitive.ObjectIDFromHex(id)
	if err != nil {
		return ErrInvalidID
	}

	user.UpdatedAt = time.Now()
	
	result, err := r.collection.ReplaceOne(ctx, bson.M{"_id": objectID}, user)
	if err != nil {
		return err
	}
	
	if result.MatchedCount == 0 {
		return ErrUserNotFound
	}
	return nil
}

DELETE (Exclusão)

// Delete remove usuário
func (r *userRepo) Delete(ctx context.Context, id string) error {
	objectID, err := primitive.ObjectIDFromHex(id)
	if err != nil {
		return ErrInvalidID
	}

	result, err := r.collection.DeleteOne(ctx, bson.M{"_id": objectID})
	if err != nil {
		return fmt.Errorf("falha ao deletar usuário: %w", err)
	}

	if result.DeletedCount == 0 {
		return ErrUserNotFound
	}

	return nil
}

// SoftDelete (delete lógico)
func (r *userRepo) SoftDelete(ctx context.Context, id string) error {
	objectID, err := primitive.ObjectIDFromHex(id)
	if err != nil {
		return ErrInvalidID
	}

	update := bson.M{
		"$set": bson.M{
			"active":     false,
			"deleted_at": time.Now(),
			"updated_at": time.Now(),
		},
	}

	result, err := r.collection.UpdateOne(ctx, bson.M{"_id": objectID}, update)
	if err != nil {
		return err
	}
	
	if result.MatchedCount == 0 {
		return ErrUserNotFound
	}
	return nil
}

// DeleteMany remove múltiplos documentos
func (r *userRepo) DeleteMany(ctx context.Context, filter bson.M) (int64, error) {
	result, err := r.collection.DeleteMany(ctx, filter)
	if err != nil {
		return 0, err
	}
	return result.DeletedCount, nil
}

Aggregation Pipeline

Pipeline Básica

// AggregateUsersStats estatísticas de usuários
func (r *userRepo) AggregateUsersStats(ctx context.Context) (*UserStats, error) {
	pipeline := mongo.Pipeline{
		// Agrupar por status ativo
		{{Key: "$group", Value: bson.M{
			"_id": "$active",
			"count": bson.M{"$sum": 1},
			"avgAge": bson.M{"$avg": "$profile.age"},
		}}},
		// Ordenar
		{{Key: "$sort", Value: bson.M{"_id": 1}}},
	}

	cursor, err := r.collection.Aggregate(ctx, pipeline)
	if err != nil {
		return nil, err
	}
	defer cursor.Close(ctx)

	var results []bson.M
	if err := cursor.All(ctx, &results); err != nil {
		return nil, err
	}

	// Processar resultados
	stats := &UserStats{}
	for _, r := range results {
		active := r["_id"].(bool)
		count := r["count"].(int32)
		if active {
			stats.ActiveUsers = int(count)
		} else {
			stats.InactiveUsers = int(count)
		}
	}

	return stats, nil
}

type UserStats struct {
	ActiveUsers   int `json:"active_users"`
	InactiveUsers int `json:"inactive_users"`
}

Pipeline Avançada com Join (Lookup)

// GetUsersWithPosts usuários com seus posts (simulação de JOIN)
func (r *userRepo) GetUsersWithPosts(ctx context.Context, userID string) (*UserWithPosts, error) {
	objectID, err := primitive.ObjectIDFromHex(userID)
	if err != nil {
		return nil, ErrInvalidID
	}

	pipeline := mongo.Pipeline{
		// Match usuário específico
		{{Key: "$match", Value: bson.M{"_id": objectID}}},
		
		// Lookup posts do usuário (coleção separada)
		{{Key: "$lookup", Value: bson.M{
			"from":         "posts",
			"localField":   "_id",
			"foreignField": "author_id",
			"as":           "posts",
		}}},
		
		// Adicionar contagem de posts
		{{Key: "$addFields", Value: bson.M{
			"posts_count": bson.M{"$size": "$posts"},
		}}},
		
		// Projetar campos desejados
		{{Key: "$project", Value: bson.M{
			"password": 0, // Excluir senha
		}}},
	}

	cursor, err := r.collection.Aggregate(ctx, pipeline)
	if err != nil {
		return nil, err
	}
	defer cursor.Close(ctx)

	var results []UserWithPosts
	if err := cursor.All(ctx, &results); err != nil {
		return nil, err
	}

	if len(results) == 0 {
		return nil, ErrUserNotFound
	}

	return &results[0], nil
}

type UserWithPosts struct {
	models.User `bson:",inline"`
	Posts       []Post `bson:"posts" json:"posts"`
	PostsCount  int    `bson:"posts_count" json:"posts_count"`
}

type Post struct {
	ID       primitive.ObjectID `bson:"_id" json:"id"`
	Title    string             `bson:"title" json:"title"`
	Content  string             `bson:"content" json:"content"`
	AuthorID primitive.ObjectID `bson:"author_id" json:"author_id"`
}
// FacetedSearch busca com facets
func (r *userRepo) FacetedSearch(ctx context.Context, query string) (*FacetedResult, error) {
	pipeline := mongo.Pipeline{
		// Match na busca
		{{Key: "$match", Value: bson.M{
			"$or": []bson.M{
				{"name": bson.M{"$regex": query, "$options": "i"}},
				{"email": bson.M{"$regex": query, "$options": "i"}},
			},
		}}},
		
		// Facet para múltiplas agregações
		{{Key: "$facet", Value: bson.M{
			"results": []bson.M{
				{"$limit": 20},
				{"$project": bson.M{"password": 0}},
			},
			"totalCount": []bson.M{
				{"$count": "count"},
			},
			"byRole": []bson.M{
				{"$unwind": "$roles"},
				{"$group": bson.M{
					"_id":   "$roles",
					"count": bson.M{"$sum": 1},
				}},
			},
			"byStatus": []bson.M{
				{"$group": bson.M{
					"_id":   "$active",
					"count": bson.M{"$sum": 1},
				}},
			},
		}},
	}

	cursor, err := r.collection.Aggregate(ctx, pipeline)
	if err != nil {
		return nil, err
	}
	defer cursor.Close(ctx)

	var results []FacetedResult
	if err := cursor.All(ctx, &results); err != nil {
		return nil, err
	}

	if len(results) == 0 {
		return &FacetedResult{}, nil
	}

	return &results[0], nil
}

type FacetedResult struct {
	Results    []models.User `bson:"results" json:"results"`
	TotalCount []struct {
		Count int `bson:"count" json:"count"`
	} `bson:"totalCount" json:"total_count"`
	ByRole []struct {
		Role  string `bson:"_id" json:"role"`
		Count int    `bson:"count" json:"count"`
	} `bson:"byRole" json:"by_role"`
	ByStatus []struct {
		Active bool `bson:"_id" json:"active"`
		Count  int  `bson:"count" json:"count"`
	} `bson:"byStatus" json:"by_status"`
}

Indexing Strategies

Criar Índices

// CreateIndexes cria índices necessários
func (r *userRepo) CreateIndexes(ctx context.Context) error {
	// Índice único para email
	emailIndex := mongo.IndexModel{
		Keys: bson.D{
			{Key: "email", Value: 1},
		},
		Options: options.Index().SetUnique(true),
	}

	// Índice composto para busca
	nameIndex := mongo.IndexModel{
		Keys: bson.D{
			{Key: "name", Value: "text"},
			{Key: "email", Value: "text"},
		},
		Options: options.Index().
			SetName("text_search").
			SetWeights(bson.M{"name": 10, "email": 5}),
	}

	// Índice para filtro de ativo + data
	activeDateIndex := mongo.IndexModel{
		Keys: bson.D{
			{Key: "active", Value: 1},
			{Key: "created_at", Value: -1},
		},
	}

	// Índice TTL para sessões (exemplo)
	ttlIndex := mongo.IndexModel{
		Keys: bson.D{
			{Key: "expires_at", Value: 1},
		},
		Options: options.Index().
			SetExpireAfterSeconds(0).
			SetName("session_ttl"),
	}

	indexes := []mongo.IndexModel{
		emailIndex,
		nameIndex,
		activeDateIndex,
		ttlIndex,
	}

	names, err := r.collection.Indexes().CreateMany(ctx, indexes)
	if err != nil {
		return fmt.Errorf("falha ao criar índices: %w", err)
	}

	log.Printf("✅ Índices criados: %v", names)
	return nil
}

Gerenciar Índices

// ListIndexes lista todos os índices
func (r *userRepo) ListIndexes(ctx context.Context) ([]bson.M, error) {
	cursor, err := r.collection.Indexes().List(ctx)
	if err != nil {
		return nil, err
	}
	defer cursor.Close(ctx)

	var indexes []bson.M
	if err := cursor.All(ctx, &indexes); err != nil {
		return nil, err
	}

	return indexes, nil
}

// DropIndex remove um índice
func (r *userRepo) DropIndex(ctx context.Context, name string) error {
	_, err := r.collection.Indexes().DropOne(ctx, name)
	return err
}

Transações

Transações Multi-Documento

// TransferSubscription transação com múltiplas operações
func (r *userRepo) TransferSubscription(
	ctx context.Context,
	fromUserID, toUserID string,
	plan string,
) error {
	// Converter IDs
	fromID, err := primitive.ObjectIDFromHex(fromUserID)
	if err != nil {
		return ErrInvalidID
	}
	toID, err := primitive.ObjectIDFromHex(toUserID)
	if err != nil {
		return ErrInvalidID
	}

	// Iniciar sessão para transação
	session, err := r.collection.Database().Client().StartSession()
	if err != nil {
		return fmt.Errorf("falha ao iniciar sessão: %w", err)
	}
	defer session.EndSession(ctx)

	// Callback da transação
	callback := func(sessCtx mongo.SessionContext) (interface{}, error) {
		// 1. Remover plano do usuário origem
		_, err := r.collection.UpdateOne(
			sessCtx,
			bson.M{"_id": fromID},
			bson.M{
				"$pull": bson.M{"subscriptions": bson.M{"plan": plan}},
				"$set":  bson.M{"updated_at": time.Now()},
			},
		)
		if err != nil {
			return nil, fmt.Errorf("falha ao remover do origem: %w", err)
		}

		// 2. Adicionar plano ao usuário destino
		_, err = r.collection.UpdateOne(
			sessCtx,
			bson.M{"_id": toID},
			bson.M{
				"$push": bson.M{"subscriptions": bson.M{
					"plan":      plan,
					"transferred_at": time.Now(),
				}},
				"$set": bson.M{"updated_at": time.Now()},
			},
		)
		if err != nil {
			return nil, fmt.Errorf("falha ao adicionar ao destino: %w", err)
		}

		// 3. Registrar transferência (outra coleção)
		transfers := r.collection.Database().Collection("transfers")
		_, err = transfers.InsertOne(sessCtx, bson.M{
			"from_user_id": fromID,
			"to_user_id":   toID,
			"plan":         plan,
			"created_at":   time.Now(),
		})
		if err != nil {
			return nil, fmt.Errorf("falha ao registrar transferência: %w", err)
		}

		return nil, nil
	}

	// Executar transação
	_, err = session.WithTransaction(ctx, callback)
	if err != nil {
		return fmt.Errorf("transação falhou: %w", err)
	}

	return nil
}

Boas Práticas

1. Context com Timeout

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

user, err := repo.GetByID(ctx, id)

2. Connection Pool

clientOptions := options.Client().
    SetMaxPoolSize(100).
    SetMinPoolSize(10).
    SetMaxConnIdleTime(30 * time.Second)

3. Tratamento de Erros

func mapMongoError(err error) error {
    if err == nil {
        return nil
    }
    
    if errors.Is(err, mongo.ErrNoDocuments) {
        return ErrNotFound
    }
    
    if mongo.IsDuplicateKeyError(err) {
        return ErrDuplicate
    }
    
    if mongo.IsTimeout(err) {
        return ErrTimeout
    }
    
    return err
}

4. Paginação Eficiente

// Use cursor-based pagination para grandes datasets
func (r *userRepo) ListWithCursor(ctx context.Context, lastID string, limit int) ([]*models.User, string, error) {
    filter := bson.M{}
    if lastID != "" {
        id, _ := primitive.ObjectIDFromHex(lastID)
        filter["_id"] = bson.M{"$gt": id}
    }
    
    opts := options.Find().
        SetLimit(int64(limit + 1)). // Buscar um extra para saber se há mais
        SetSort(bson.M{"_id": 1})
    
    cursor, err := r.collection.Find(ctx, filter, opts)
    if err != nil {
        return nil, "", err
    }
    defer cursor.Close(ctx)
    
    var users []*models.User
    if err := cursor.All(ctx, &users); err != nil {
        return nil, "", err
    }
    
    var nextCursor string
    if len(users) > limit {
        nextCursor = users[limit].ID.Hex()
        users = users[:limit]
    }
    
    return users, nextCursor, nil
}

5. Projeções para Performance

// Retornar apenas campos necessários
opts := options.Find().SetProjection(bson.M{
    "password": 0,      // Excluir
    "metadata": 0,      // Excluir
    "name":     1,      // Incluir
    "email":    1,      // Incluir
})

Checklist para Produção

  • Índices criados para queries frequentes
  • Connection pool configurado adequadamente
  • Timeouts em todas as operações
  • Transações para operações multi-documento
  • Projeções para limitar dados transferidos
  • Paginação para grandes datasets
  • Replica Set configurado (não standalone)
  • Autenticação habilitada
  • TLS/SSL para conexões
  • Backup automatizado

Próximos Passos

Aprofunde seus conhecimentos:

  1. Go e PostgreSQL - Banco relacional com Go
  2. Go e Redis - Cache e session store
  3. Go e Kafka - Event streaming
  4. Go Observability - Logs e métricas

Go + MongoDB: flexibilidade e performance para aplicações modernas. Compartilhe seu projeto!