Go e MongoDB: CRUD e Agregações Completas

Aprenda a integrar Go com MongoDB de forma profissional. Domine operações CRUD, aggregation pipeline, indexing, transações e padrões de produção com o driver oficial.

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:

Ler mais →

Go e PostgreSQL: CRUD Completo com Boas Práticas

Aprenda a integrar Go com PostgreSQL de forma profissional. Domine CRUD operations, transações, migrations, query builders e a ferramenta sqlc para type-safe SQL.

PostgreSQL é um dos bancos de dados relacionais mais robustos e populares do mundo. Combinado com Go, você tem uma stack poderosa para aplicações de alta performance. Neste guia, você aprenderá todas as técnicas essenciais para trabalhar com PostgreSQL em Go, desde operações básicas até padrões avançados de produção.

Por Que PostgreSQL com Go?

O Match Perfeito

┌─────────────────────────────────────────────────────────────────┐
│  POSTGRESQL          │  GO                                     │
├─────────────────────────────────────────────────────────────────┤
│  Confiável           │  Type-safe, compilação rigorosa         │
├─────────────────────────────────────────────────────────────────┤
│  ACID transactions   │  Error handling explícito               │
├─────────────────────────────────────────────────────────────────┤
│  Alto desempenho     │  Runtime eficiente, baixa latência      │
├─────────────────────────────────────────────────────────────────┤
│  Extensível          │  Interfaces flexíveis                   │
├─────────────────────────────────────────────────────────────────┤
│  Open source         │  Open source                            │
└─────────────────────────────────────────────────────────────────┘

Configuração do Projeto

Instalação das Dependências

# Driver padrão PostgreSQL
go get -u github.com/lib/pq

# Driver alternativo (mais performático)
go get -u github.com/jackc/pgx/v5

# sqlc - type-safe SQL
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest

# Migration tool
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest

Estrutura do Projeto

project/
├── cmd/
│   └── api/
│       └── main.go
├── internal/
│   ├── config/
│   │   └── database.go
│   ├── models/
│   │   └── user.go
│   ├── repository/
│   │   └── user_repository.go
│   └── service/
│       └── user_service.go
├── migrations/
│   ├── 001_create_users_table.up.sql
│   ├── 001_create_users_table.down.sql
│   └── ...
├── queries/
│   └── user.sql
├── sqlc.yaml
├── go.mod
└── .env

Conexão com PostgreSQL

Usando database/sql (Padrão)

// internal/config/database.go
package config

import (
	"database/sql"
	"fmt"
	"log"
	"os"
	"time"

	_ "github.com/lib/pq"
)

// Configurações do banco
type DBConfig struct {
	Host     string
	Port     int
	User     string
	Password string
	Database string
	SSLMode  string
}

func NewDBConfig() *DBConfig {
	return &DBConfig{
		Host:     getEnv("DB_HOST", "localhost"),
		Port:     getEnvAsInt("DB_PORT", 5432),
		User:     getEnv("DB_USER", "postgres"),
		Password: getEnv("DB_PASSWORD", "password"),
		Database: getEnv("DB_NAME", "myapp"),
		SSLMode:  getEnv("DB_SSLMODE", "disable"),
	}
}

func (c *DBConfig) ConnectionString() string {
	return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
		c.Host, c.Port, c.User, c.Password, c.Database, c.SSLMode)
}

// Conexão com pool
func NewDB(cfg *DBConfig) (*sql.DB, error) {
	db, err := sql.Open("postgres", cfg.ConnectionString())
	if err != nil {
		return nil, fmt.Errorf("falha ao abrir conexão: %w", err)
	}

	// Configurar pool de conexões
	db.SetMaxOpenConns(25)           // Máximo de conexões abertas
	db.SetMaxIdleConns(10)           // Máximo de conexões idle
	db.SetConnMaxLifetime(time.Hour) // Tempo máximo de vida
	db.SetConnMaxIdleTime(30 * time.Minute)

	// Verificar conexão
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	if err := db.PingContext(ctx); err != nil {
		return nil, fmt.Errorf("falha ao ping banco de dados: %w", err)
	}

	log.Println("✅ Conectado ao PostgreSQL")
	return db, nil
}

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

func getEnvAsInt(key string, defaultValue int) int {
	if value := os.Getenv(key); value != "" {
		var intValue int
		fmt.Sscanf(value, "%d", &intValue)
		return intValue
	}
	return defaultValue
}

Usando pgx (Recomendado)

// internal/config/pgx_database.go
package config

import (
	"context"
	"fmt"
	"log"

	"github.com/jackc/pgx/v5/pgxpool"
)

func NewPgxPool(cfg *DBConfig) (*pgxpool.Pool, error) {
	connString := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s&pool_max_conns=25",
		cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Database, cfg.SSLMode)

	pool, err := pgxpool.New(context.Background(), connString)
	if err != nil {
		return nil, fmt.Errorf("falha ao criar pool: %w", err)
	}

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

	log.Println("✅ Conectado ao PostgreSQL com pgx")
	return pool, nil
}

Modelos e Estruturas

// internal/models/user.go
package models

import (
	"time"
)

// User representa um usuário no banco
type User struct {
	ID        int64     `json:"id"`
	Email     string    `json:"email"`
	Name      string    `json:"name"`
	Password  string    `json:"-"` // Não serializar
	Active    bool      `json:"active"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

// UserCreate é usado para criar usuários
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"`
}

// UserUpdate é usado para atualizar usuários
type UserUpdate struct {
	Email  *string `json:"email,omitempty"`
	Name   *string `json:"name,omitempty"`
	Active *bool   `json:"active,omitempty"`
}

// TableName retorna o nome da tabela
func (User) TableName() string {
	return "users"
}

Operações CRUD

Repository Pattern

// internal/repository/user_repository.go
package repository

import (
	"context"
	"database/sql"
	"errors"
	"fmt"

	"project/internal/models"
)

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

// UserRepository define operações de banco
type UserRepository interface {
	Create(ctx context.Context, user *models.UserCreate) (*models.User, error)
	GetByID(ctx context.Context, id int64) (*models.User, error)
	GetByEmail(ctx context.Context, email string) (*models.User, error)
	List(ctx context.Context, limit, offset int) ([]*models.User, error)
	Update(ctx context.Context, id int64, update *models.UserUpdate) (*models.User, error)
	Delete(ctx context.Context, id int64) error
	Count(ctx context.Context) (int64, error)
}

// userRepo implementa UserRepository
type userRepo struct {
	db *sql.DB
}

// NewUserRepository cria nova instância
func NewUserRepository(db *sql.DB) UserRepository {
	return &userRepo{db: db}
}

CREATE

// Create insere novo usuário
func (r *userRepo) Create(ctx context.Context, user *models.UserCreate) (*models.User, error) {
	query := `
		INSERT INTO users (email, name, password, active, created_at, updated_at)
		VALUES ($1, $2, $3, $4, NOW(), NOW())
		RETURNING id, email, name, active, created_at, updated_at
	`

	// Em produção, faça hash da senha!
	hashedPassword := hashPassword(user.Password)

	var created models.User
	err := r.db.QueryRowContext(ctx, query,
		user.Email,
		user.Name,
		hashedPassword,
		true,
	).Scan(
		&created.ID,
		&created.Email,
		&created.Name,
		&created.Active,
		&created.CreatedAt,
		&created.UpdatedAt,
	)

	if err != nil {
		// Verificar erro de duplicação
		if isDuplicateKeyError(err) {
			return nil, ErrEmailExists
		}
		return nil, fmt.Errorf("falha ao criar usuário: %w", err)
	}

	return &created, nil
}

func hashPassword(password string) string {
	// Use bcrypt em produção!
	// import "golang.org/x/crypto/bcrypt"
	// hashed, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	// return string(hashed)
	return "hashed_" + password
}

func isDuplicateKeyError(err error) bool {
	if err == nil {
		return false
	}
	// Verificar código de erro PostgreSQL para duplicação
	return strings.Contains(err.Error(), "duplicate key")
}

READ

// GetByID busca usuário por ID
func (r *userRepo) GetByID(ctx context.Context, id int64) (*models.User, error) {
	query := `
		SELECT id, email, name, active, created_at, updated_at
		FROM users
		WHERE id = $1
	`

	var user models.User
	err := r.db.QueryRowContext(ctx, query, id).Scan(
		&user.ID,
		&user.Email,
		&user.Name,
		&user.Active,
		&user.CreatedAt,
		&user.UpdatedAt,
	)

	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, ErrUserNotFound
		}
		return nil, fmt.Errorf("falha ao buscar usuário: %w", err)
	}

	return &user, nil
}

// GetByEmail busca usuário por email
func (r *userRepo) GetByEmail(ctx context.Context, email string) (*models.User, error) {
	query := `
		SELECT id, email, name, active, created_at, updated_at
		FROM users
		WHERE email = $1
	`

	var user models.User
	err := r.db.QueryRowContext(ctx, query, email).Scan(
		&user.ID,
		&user.Email,
		&user.Name,
		&user.Active,
		&user.CreatedAt,
		&user.UpdatedAt,
	)

	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, ErrUserNotFound
		}
		return nil, fmt.Errorf("falha ao buscar usuário: %w", err)
	}

	return &user, nil
}

// List retorna lista paginada de usuários
func (r *userRepo) List(ctx context.Context, limit, offset int) ([]*models.User, error) {
	query := `
		SELECT id, email, name, active, created_at, updated_at
		FROM users
		ORDER BY created_at DESC
		LIMIT $1 OFFSET $2
	`

	if limit <= 0 {
		limit = 10
	}
	if limit > 100 {
		limit = 100 // Máximo por página
	}

	rows, err := r.db.QueryContext(ctx, query, limit, offset)
	if err != nil {
		return nil, fmt.Errorf("falha ao listar usuários: %w", err)
	}
	defer rows.Close()

	var users []*models.User
	for rows.Next() {
		var user models.User
		err := rows.Scan(
			&user.ID,
			&user.Email,
			&user.Name,
			&user.Active,
			&user.CreatedAt,
			&user.UpdatedAt,
		)
		if err != nil {
			return nil, fmt.Errorf("falha ao scan usuário: %w", err)
		}
		users = append(users, &user)
	}

	if err = rows.Err(); err != nil {
		return nil, fmt.Errorf("erro ao iterar rows: %w", err)
	}

	return users, nil
}

// Count retorna total de usuários
func (r *userRepo) Count(ctx context.Context) (int64, error) {
	query := `SELECT COUNT(*) FROM users`

	var count int64
	err := r.db.QueryRowContext(ctx, query).Scan(&count)
	if err != nil {
		return 0, fmt.Errorf("falha ao contar usuários: %w", err)
	}

	return count, nil
}

UPDATE

// Update atualiza usuário
func (r *userRepo) Update(ctx context.Context, id int64, update *models.UserUpdate) (*models.User, error) {
	// Construir query dinamicamente
	var setParts []string
	var args []interface{}
	argIndex := 1

	if update.Email != nil {
		setParts = append(setParts, fmt.Sprintf("email = $%d", argIndex))
		args = append(args, *update.Email)
		argIndex++
	}
	if update.Name != nil {
		setParts = append(setParts, fmt.Sprintf("name = $%d", argIndex))
		args = append(args, *update.Name)
		argIndex++
	}
	if update.Active != nil {
		setParts = append(setParts, fmt.Sprintf("active = $%d", argIndex))
		args = append(args, *update.Active)
		argIndex++
	}

	if len(setParts) == 0 {
		return nil, errors.New("nenhum campo para atualizar")
	}

	// Sempre atualizar updated_at
	setParts = append(setParts, fmt.Sprintf("updated_at = $%d", argIndex))
	args = append(args, time.Now())
	argIndex++

	// Adicionar ID
	args = append(args, id)

	query := fmt.Sprintf(`
		UPDATE users
		SET %s
		WHERE id = $%d
		RETURNING id, email, name, active, created_at, updated_at
	`, strings.Join(setParts, ", "), argIndex)

	var user models.User
	err := r.db.QueryRowContext(ctx, query, args...).Scan(
		&user.ID,
		&user.Email,
		&user.Name,
		&user.Active,
		&user.CreatedAt,
		&user.UpdatedAt,
	)

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

	return &user, nil
}

DELETE

// Delete remove usuário
func (r *userRepo) Delete(ctx context.Context, id int64) error {
	query := `DELETE FROM users WHERE id = $1`

	result, err := r.db.ExecContext(ctx, query, id)
	if err != nil {
		return fmt.Errorf("falha ao deletar usuário: %w", err)
	}

	rowsAffected, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("falha ao obter rows afetadas: %w", err)
	}

	if rowsAffected == 0 {
		return ErrUserNotFound
	}

	return nil
}

// SoftDelete (alternativa ao delete físico)
func (r *userRepo) SoftDelete(ctx context.Context, id int64) error {
	query := `
		UPDATE users 
		SET active = false, deleted_at = NOW() 
		WHERE id = $1
	`

	result, err := r.db.ExecContext(ctx, query, id)
	if err != nil {
		return fmt.Errorf("falha ao deletar usuário: %w", err)
	}

	rowsAffected, _ := result.RowsAffected()
	if rowsAffected == 0 {
		return ErrUserNotFound
	}

	return nil
}

Transações

// WithTransaction executa função dentro de transação
func (r *userRepo) WithTransaction(ctx context.Context, fn func(*sql.Tx) error) error {
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return fmt.Errorf("falha ao iniciar transação: %w", err)
	}

	defer func() {
		if p := recover(); p != nil {
			tx.Rollback()
			panic(p)
		}
	}()

	if err := fn(tx); err != nil {
		tx.Rollback()
		return err
	}

	if err := tx.Commit(); err != nil {
		return fmt.Errorf("falha ao commit: %w", err)
	}

	return nil
}

// TransferBalance exemplo de transação
func TransferBalance(ctx context.Context, db *sql.DB, fromID, toID int64, amount float64) error {
	tx, err := db.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer tx.Rollback()

	// Verificar saldo
	var balance float64
	err = tx.QueryRowContext(ctx, 
		"SELECT balance FROM accounts WHERE id = $1 FOR UPDATE", 
		fromID,
	).Scan(&balance)
	if err != nil {
		return err
	}

	if balance < amount {
		return errors.New("saldo insuficiente")
	}

	// Debitar
	_, err = tx.ExecContext(ctx,
		"UPDATE accounts SET balance = balance - $1 WHERE id = $2",
		amount, fromID,
	)
	if err != nil {
		return err
	}

	// Creditar
	_, err = tx.ExecContext(ctx,
		"UPDATE accounts SET balance = balance + $1 WHERE id = $2",
		amount, toID,
	)
	if err != nil {
		return err
	}

	// Registrar transação
	_, err = tx.ExecContext(ctx,
		`INSERT INTO transactions (from_id, to_id, amount, created_at) 
		 VALUES ($1, $2, $3, NOW())`,
		fromID, toID, amount,
	)
	if err != nil {
		return err
	}

	return tx.Commit()
}

sqlc: Type-Safe SQL

Configuração

# sqlc.yaml
version: "2"
sql:
  - schema: "migrations/"
    queries: "queries/"
    engine: "postgresql"
    gen:
      go:
        package: "db"
        out: "internal/db"
        sql_package: "pgx/v5"
        emit_json_tags: true
        emit_prepared_queries: true
        emit_interface: true
        emit_empty_slices: true

Queries SQL

-- queries/user.sql
-- name: CreateUser :one
INSERT INTO users (email, name, password, active)
VALUES ($1, $2, $3, $4)
RETURNING *;

-- name: GetUser :one
SELECT * FROM users
WHERE id = $1;

-- name: GetUserByEmail :one
SELECT * FROM users
WHERE email = $1;

-- name: ListUsers :many
SELECT * FROM users
ORDER BY created_at DESC
LIMIT $1 OFFSET $2;

-- name: UpdateUser :one
UPDATE users
SET email = COALESCE($2, email),
    name = COALESCE($3, name),
    active = COALESCE($4, active),
    updated_at = NOW()
WHERE id = $1
RETURNING *;

-- name: DeleteUser :exec
DELETE FROM users WHERE id = $1;

-- name: CountUsers :one
SELECT COUNT(*) FROM users;

Gerar Código

# Gerar código Go a partir das queries
sqlc generate

# Estrutura gerada:
# internal/db/
# ├── db.go
# ├── models.go
# ├── querier.go (interface)
# └── user.sql.go

Uso do sqlc

// internal/repository/user_sqlc.go
package repository

import (
	"context"

	"github.com/jackc/pgx/v5/pgtype"
	"project/internal/db"
)

type UserSQLCRepository struct {
	queries *db.Queries
}

func NewUserSQLCRepository(queries *db.Queries) *UserSQLCRepository {
	return &UserSQLCRepository{queries: queries}
}

func (r *UserSQLCRepository) Create(ctx context.Context, email, name, password string) (*db.User, error) {
	user, err := r.queries.CreateUser(ctx, db.CreateUserParams{
		Email:    email,
		Name:     name,
		Password: password,
		Active:   true,
	})
	if err != nil {
		return nil, err
	}
	return &user, nil
}

func (r *UserSQLCRepository) List(ctx context.Context, limit, offset int32) ([]db.User, error) {
	return r.queries.ListUsers(ctx, db.ListUsersParams{
		Limit:  limit,
		Offset: offset,
	})
}

func (r *UserSQLCRepository) Update(ctx context.Context, id int64, email, name *string, active *bool) (*db.User, error) {
	var emailParam, nameParam pgtype.Text
	var activeParam pgtype.Bool

	if email != nil {
		emailParam = pgtype.Text{String: *email, Valid: true}
	}
	if name != nil {
		nameParam = pgtype.Text{String: *name, Valid: true}
	}
	if active != nil {
		activeParam = pgtype.Bool{Bool: *active, Valid: true}
	}

	user, err := r.queries.UpdateUser(ctx, db.UpdateUserParams{
		ID:     id,
		Email:  emailParam,
		Name:   nameParam,
		Active: activeParam,
	})
	if err != nil {
		return nil, err
	}
	return &user, nil
}

Migrations

Usando golang-migrate

# Criar nova migration
migrate create -ext sql -dir migrations -seq create_users_table

# Aplicar migrations
migrate -path migrations -database "postgres://user:pass@localhost/db?sslmode=disable" up

# Reverter última migration
migrate -path migrations -database "postgres://user:pass@localhost/db?sslmode=disable" down 1

Migration Files

-- migrations/001_create_users_table.up.sql
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    name VARCHAR(100) NOT NULL,
    password VARCHAR(255) NOT NULL,
    active BOOLEAN DEFAULT true,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    deleted_at TIMESTAMP WITH TIME ZONE
);

CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_active ON users(active);

-- Trigger para atualizar updated_at
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ language 'plpgsql';

CREATE TRIGGER update_users_updated_at 
    BEFORE UPDATE ON users 
    FOR EACH ROW 
    EXECUTE FUNCTION update_updated_at_column();
-- migrations/001_create_users_table.down.sql
DROP TRIGGER IF EXISTS update_users_updated_at ON users;
DROP FUNCTION IF EXISTS update_updated_at_column();
DROP INDEX IF EXISTS idx_users_active;
DROP INDEX IF EXISTS idx_users_email;
DROP TABLE IF EXISTS users;

Boas Práticas

1. Context para Timeout

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

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

2. Prepared Statements

// database/sql já usa prepared statements internamente
// Para queries repetidas, use Prepare

stmt, err := db.PrepareContext(ctx, "SELECT * FROM users WHERE id = $1")
if err != nil {
    return err
}
defer stmt.Close()

for _, id := range ids {
    row := stmt.QueryRowContext(ctx, id)
    // ...
}

3. Connection Pool

db.SetMaxOpenConns(25)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(time.Hour)

4. Error Handling

var (
    ErrNotFound = errors.New("recurso não encontrado")
    ErrDuplicate = errors.New("recurso já existe")
    ErrConstraint = errors.New("violação de constraint")
)

func mapError(err error) error {
    if err == nil {
        return nil
    }
    
    if errors.Is(err, sql.ErrNoRows) {
        return ErrNotFound
    }
    
    pqErr, ok := err.(*pq.Error)
    if ok {
        switch pqErr.Code {
        case "23505": // unique_violation
            return ErrDuplicate
        case "23503": // foreign_key_violation
            return ErrConstraint
        }
    }
    
    return err
}

5. Logging

// Log queries em desenvolvimento
func logQuery(query string, args ...interface{}) {
    if os.Getenv("ENV") == "development" {
        log.Printf("[SQL] %s | Args: %v", query, args)
    }
}

Checklist para Produção

  • Connection pool configurado corretamente
  • Timeouts em todas as queries (context)
  • Migrations automatizadas no deploy
  • Prepared statements para queries frequentes
  • Índices otimizados
  • Hash de senhas (bcrypt/argon2)
  • Soft delete para dados importantes
  • Auditoria (created_at, updated_at)
  • SSL para conexões (sslmode=require)
  • Backup automatizado

Próximos Passos

Aprofunde seus conhecimentos:

Ler mais →