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
| Aspecto | MongoDB | PostgreSQL |
|---|---|---|
| Esquema | Flexível (schema-less) | Rígido (schema-full) |
| Relacionamentos | Embutidos ou referências | JOINs nativos |
| Escalabilidade | Horizontal (sharding) | Vertical + Replicação |
| Casos de uso | CMS, IoT, Analytics | Financeiro, ERP, ACID |
| Performance | Excelente para reads | Excelente 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"`
}
Faceted Search
// 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:
- Go e PostgreSQL - Banco relacional com Go
- Go e Redis - Cache e session store
- Go e Kafka - Event streaming
- Go Observability - Logs e métricas
Go + MongoDB: flexibilidade e performance para aplicações modernas. Compartilhe seu projeto!