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:

  1. Go CLI Tools - Ferramentas de linha de comando
  2. Go e gRPC - APIs de alta performance
  3. Go para Microserviços - Arquitetura distribuída
  4. Go Observability - Logs e métricas

Go + PostgreSQL: performance e confiabilidade em produção. Compartilhe seu projeto!