Golang

Go com PostgreSQL: recursos avançados e o driver pgx Já leu

11 min de leitura

Go com PostgreSQL: recursos avançados e o driver pgx
PostgreSQL é frequentemente descrito como o banco de dados relacional mais completo disponível. Suporte nativo a JSON/JSONB, arrays, tipos geométricos, full-text search, extensões como PostGIS e pgvector, transações ACID

PostgreSQL é frequentemente descrito como o banco de dados relacional mais completo disponível. Suporte nativo a JSON/JSONB, arrays, tipos geométricos, full-text search, extensões como PostGIS e pgvector, transações ACID robustas, replicação lógica e um sistema de tipos extensível fazem dele a escolha preferida para aplicações que precisam de mais do que tabelas e índices simples.

Em Go, o driver de referência é o pgx — escrito especificamente para PostgreSQL, sem tentar ser genérico. Ele expõe recursos do PostgreSQL que o database/sql padrão não alcança, como tipos nativos, COPY protocol, listen/notify e prepared statements com cache automático.


Dois modos de uso do pgx

O pgx pode ser usado de duas formas:

Modo database/sql — compatível com a interface padrão, mas perde alguns recursos avançados:

go get github.com/jackc/pgx/v5
go get github.com/jackc/pgx/v5/stdlib

Modo nativo — acesso completo a todos os recursos do PostgreSQL:

go get github.com/jackc/pgx/v5

Este artigo usa principalmente o modo nativo, que é o recomendado para novos projetos.


Configuração do ambiente

# PostgreSQL com Docker
docker run --name postgres-dev \
    -e POSTGRES_USER=usuario \
    -e POSTGRES_PASSWORD=senha \
    -e POSTGRES_DB=app \
    -p 5432:5432 \
    -d postgres:16

Conexão com pgxpool

O pgx oferece pgxpool — um pool de conexões nativo otimizado para PostgreSQL:

package main

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

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

func conectarPostgres(ctx context.Context) (*pgxpool.Pool, error) {
    // DSN do PostgreSQL
    dsn := "postgres://usuario:senha@localhost:5432/app?sslmode=disable"

    config, err := pgxpool.ParseConfig(dsn)
    if err != nil {
        return nil, fmt.Errorf("parsear config: %w", err)
    }

    // Configurações do pool
    config.MaxConns = 25
    config.MinConns = 5
    config.MaxConnLifetime = 5 * time.Minute
    config.MaxConnIdleTime = 2 * time.Minute
    config.HealthCheckPeriod = 1 * time.Minute

    // Hook de inicialização de cada conexão
    config.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error {
        // Configura o timezone da conexão
        _, err := conn.Exec(ctx, "SET timezone = 'America/Sao_Paulo'")
        return err
    }

    pool, err := pgxpool.NewWithConfig(ctx, config)
    if err != nil {
        return nil, fmt.Errorf("criar pool: %w", err)
    }

    // Verifica conectividade
    if err := pool.Ping(ctx); err != nil {
        pool.Close()
        return nil, fmt.Errorf("verificar conexão: %w", err)
    }

    log.Println("PostgreSQL conectado")
    return pool, nil
}

func main() {
    ctx := context.Background()

    pool, err := conectarPostgres(ctx)
    if err != nil {
        log.Fatal(err)
    }
    defer pool.Close()

    var versao string
    err = pool.QueryRow(ctx, "SELECT version()").Scan(&versao)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(versao)
}

Schema PostgreSQL com tipos avançados

CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

CREATE TABLE usuarios (
    id         UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    nome       TEXT NOT NULL,
    email      TEXT NOT NULL UNIQUE,
    metadata   JSONB,
    tags       TEXT[],
    criado_em  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    ativo      BOOLEAN NOT NULL DEFAULT TRUE
);

CREATE TABLE posts (
    id          UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    autor_id    UUID NOT NULL REFERENCES usuarios(id),
    titulo      TEXT NOT NULL,
    conteudo    TEXT NOT NULL,
    status      TEXT NOT NULL DEFAULT 'rascunho'
                CHECK(status IN ('rascunho', 'publicado', 'arquivado')),
    publicado_em TIMESTAMPTZ,
    criado_em   TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    tsv         TSVECTOR GENERATED ALWAYS AS (
                    to_tsvector('portuguese', titulo || ' ' || conteudo)
                ) STORED
);

CREATE INDEX idx_posts_tsv ON posts USING GIN(tsv);
CREATE INDEX idx_usuarios_tags ON usuarios USING GIN(tags);
CREATE INDEX idx_usuarios_metadata ON usuarios USING GIN(metadata);

Tipos nativos do pgx

O pgx mapeia tipos PostgreSQL para tipos Go nativos sem conversão manual:

package main

import (
    "context"
    "time"

    "github.com/google/uuid"
    "github.com/jackc/pgx/v5"
    "github.com/jackc/pgx/v5/pgtype"
    "github.com/jackc/pgx/v5/pgxpool"
)

type Usuario struct {
    ID        pgtype.UUID
    Nome      string
    Email     string
    Metadata  map[string]any
    Tags      []string
    CriadoEm time.Time
    Ativo     bool
}

func criarUsuario(ctx context.Context, pool *pgxpool.Pool, nome, email string, tags []string) (*Usuario, error) {
    query := `
        INSERT INTO usuarios (nome, email, tags)
        VALUES (@nome, @email, @tags)
        RETURNING id, nome, email, metadata, tags, criado_em, ativo
    `

    // pgx suporta parâmetros nomeados com @nome
    args := pgx.NamedArgs{
        "nome":  nome,
        "email": email,
        "tags":  tags,
    }

    var u Usuario
    err := pool.QueryRow(ctx, query, args).Scan(
        &u.ID,
        &u.Nome,
        &u.Email,
        &u.Metadata,
        &u.Tags,
        &u.CriadoEm,
        &u.Ativo,
    )
    if err != nil {
        return nil, fmt.Errorf("criar usuário: %w", err)
    }

    return &u, nil
}

JSONB: documentos dentro do relacional

type ConfiguracaoUsuario struct {
    Tema        string            `json:"tema"`
    Idioma      string            `json:"idioma"`
    Notificacoes map[string]bool  `json:"notificacoes"`
}

func atualizarMetadata(ctx context.Context, pool *pgxpool.Pool,
    userID pgtype.UUID, config ConfiguracaoUsuario) error {

    query := `
        UPDATE usuarios
        SET metadata = metadata || @config::jsonb
        WHERE id = @id
    `

    dados, err := json.Marshal(config)
    if err != nil {
        return err
    }

    _, err = pool.Exec(ctx, query, pgx.NamedArgs{
        "config": string(dados),
        "id":     userID,
    })
    return err
}

func buscarPorMetadata(ctx context.Context, pool *pgxpool.Pool,
    tema string) ([]Usuario, error) {

    // Consulta JSONB com operador @>
    query := `
        SELECT id, nome, email, tags, criado_em
        FROM usuarios
        WHERE metadata @> @filtro::jsonb
        AND ativo = true
    `

    filtro := fmt.Sprintf(`{"tema": "%s"}`, tema)

    rows, err := pool.Query(ctx, query, pgx.NamedArgs{"filtro": filtro})
    if err != nil {
        return nil, fmt.Errorf("buscar por metadata: %w", err)
    }
    defer rows.Close()

    return pgx.CollectRows(rows, pgx.RowToStructByName[Usuario])
}

pgx.CollectRows: scan automático em structs

O pgx oferece utilitários para mapear rows diretamente em structs sem scan manual:

type Post struct {
    ID          pgtype.UUID `db:"id"`
    AutorID     pgtype.UUID `db:"autor_id"`
    Titulo      string      `db:"titulo"`
    Conteudo    string      `db:"conteudo"`
    Status      string      `db:"status"`
    PublicadoEm pgtype.Timestamptz `db:"publicado_em"`
    CriadoEm   time.Time   `db:"criado_em"`
}

func listarPosts(ctx context.Context, pool *pgxpool.Pool, status string, limite int) ([]Post, error) {
    query := `
        SELECT id, autor_id, titulo, conteudo, status, publicado_em, criado_em
        FROM posts
        WHERE status = @status
        ORDER BY criado_em DESC
        LIMIT @limite
    `

    rows, err := pool.Query(ctx, query, pgx.NamedArgs{
        "status": status,
        "limite": limite,
    })
    if err != nil {
        return nil, fmt.Errorf("listar posts: %w", err)
    }
    defer rows.Close()

    // CollectRows elimina o loop manual de rows.Next() + Scan()
    posts, err := pgx.CollectRows(rows, pgx.RowToStructByName[Post])
    if err != nil {
        return nil, fmt.Errorf("coletar posts: %w", err)
    }

    return posts, nil
}

Full-text search nativo

type ResultadoBusca struct {
    ID        pgtype.UUID
    Titulo    string
    Relevancia float64
    Trecho    string
}

func buscarPosts(ctx context.Context, pool *pgxpool.Pool, termo string) ([]ResultadoBusca, error) {
    query := `
        SELECT
            id,
            titulo,
            ts_rank(tsv, query) AS relevancia,
            ts_headline('portuguese', conteudo, query,
                'MaxWords=50, MinWords=20, StartSel=<b>, StopSel=</b>'
            ) AS trecho
        FROM posts, to_tsquery('portuguese', @termo) query
        WHERE tsv @@ query
        AND status = 'publicado'
        ORDER BY relevancia DESC
        LIMIT 10
    `

    rows, err := pool.Query(ctx, query, pgx.NamedArgs{"termo": termo})
    if err != nil {
        return nil, fmt.Errorf("buscar posts: %w", err)
    }
    defer rows.Close()

    var resultados []ResultadoBusca
    for rows.Next() {
        var r ResultadoBusca
        if err := rows.Scan(&r.ID, &r.Titulo, &r.Relevancia, &r.Trecho); err != nil {
            return nil, err
        }
        resultados = append(resultados, r)
    }

    return resultados, rows.Err()
}

Transações com pgx

func publicarPost(ctx context.Context, pool *pgxpool.Pool, postID pgtype.UUID) error {
    tx, err := pool.Begin(ctx)
    if err != nil {
        return fmt.Errorf("iniciar transação: %w", err)
    }
    defer tx.Rollback(ctx) // No-op se já commitado

    // Verifica se o post existe e está como rascunho
    var status string
    err = tx.QueryRow(ctx,
        "SELECT status FROM posts WHERE id = $1 FOR UPDATE",
        postID,
    ).Scan(&status)

    if err == pgx.ErrNoRows {
        return fmt.Errorf("post não encontrado")
    }
    if err != nil {
        return fmt.Errorf("verificar post: %w", err)
    }
    if status != "rascunho" {
        return fmt.Errorf("post não está em rascunho: %s", status)
    }

    // Publica o post
    _, err = tx.Exec(ctx, `
        UPDATE posts
        SET status = 'publicado', publicado_em = NOW()
        WHERE id = $1
    `, postID)
    if err != nil {
        return fmt.Errorf("publicar post: %w", err)
    }

    // Registra evento de auditoria
    _, err = tx.Exec(ctx, `
        INSERT INTO auditoria (entidade, entidade_id, acao, realizado_em)
        VALUES ('post', $1, 'publicacao', NOW())
    `, postID)
    if err != nil {
        return fmt.Errorf("registrar auditoria: %w", err)
    }

    return tx.Commit(ctx)
}

Listen/Notify: eventos em tempo real

PostgreSQL suporta um mecanismo de pub/sub nativo através de LISTEN e NOTIFY. O pgx expõe isso de forma nativa:

package main

import (
    "context"
    "fmt"
    "log"

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

func escutarEventos(ctx context.Context, connStr string) error {
    // Listen/Notify requer uma conexão dedicada — não use o pool
    conn, err := pgx.Connect(ctx, connStr)
    if err != nil {
        return fmt.Errorf("conectar: %w", err)
    }
    defer conn.Close(ctx)

    // Registra para receber notificações do canal "novos_pedidos"
    _, err = conn.Exec(ctx, "LISTEN novos_pedidos")
    if err != nil {
        return fmt.Errorf("registrar listener: %w", err)
    }

    log.Println("Aguardando eventos de novos pedidos...")

    for {
        // WaitForNotification bloqueia até receber uma notificação
        notificacao, err := conn.WaitForNotification(ctx)
        if err != nil {
            if ctx.Err() != nil {
                return nil // contexto cancelado — encerramento normal
            }
            return fmt.Errorf("aguardar notificação: %w", err)
        }

        fmt.Printf("Canal: %s | PID: %d | Payload: %s\n",
            notificacao.Channel,
            notificacao.PID,
            notificacao.Payload,
        )
    }
}

// No lado do emissor — pode ser chamado de qualquer conexão
func notificarNovoPedido(ctx context.Context, pool *pgxpool.Pool, pedidoID string) error {
    _, err := pool.Exec(ctx,
        "SELECT pg_notify('novos_pedidos', $1)", pedidoID)
    return err
}

COPY: inserção em massa de alta performance

Para inserir grandes volumes de dados, o protocolo COPY é ordens de magnitude mais rápido que INSERTs individuais:

func inserirUsuariosEmMassa(ctx context.Context, pool *pgxpool.Pool, usuarios []Usuario) error {
    conn, err := pool.Acquire(ctx)
    if err != nil {
        return fmt.Errorf("adquirir conexão: %w", err)
    }
    defer conn.Release()

    // CopyFrom usa o protocolo COPY do PostgreSQL
    linhas := make([][]any, len(usuarios))
    for i, u := range usuarios {
        linhas[i] = []any{u.Nome, u.Email, u.Tags}
    }

    copiados, err := conn.Conn().CopyFrom(
        ctx,
        pgx.Identifier{"usuarios"},
        []string{"nome", "email", "tags"},
        pgx.CopyFromRows(linhas),
    )
    if err != nil {
        return fmt.Errorf("copy from: %w", err)
    }

    fmt.Printf("%d usuários inseridos\n", copiados)
    return nil
}

Tratando erros específicos do PostgreSQL

import "github.com/jackc/pgx/v5/pgconn"

func tratarErroPG(err error) error {
    var pgErr *pgconn.PgError
    if errors.As(err, &pgErr) {
        switch pgErr.Code {
        case "23505": // unique_violation
            return fmt.Errorf("registro duplicado: %s", pgErr.ConstraintName)
        case "23503": // foreign_key_violation
            return fmt.Errorf("referência inválida: %s", pgErr.ConstraintName)
        case "23502": // not_null_violation
            return fmt.Errorf("campo obrigatório ausente: %s", pgErr.ColumnName)
        case "42P01": // undefined_table
            return fmt.Errorf("tabela não existe: %s", pgErr.TableName)
        default:
            return fmt.Errorf("erro PostgreSQL %s: %s", pgErr.Code, pgErr.Message)
        }
    }
    return err
}

func criarUsuarioComTratamento(ctx context.Context, pool *pgxpool.Pool,
    nome, email string) (*Usuario, error) {

    u, err := criarUsuario(ctx, pool, nome, email, nil)
    if err != nil {
        return nil, tratarErroPG(err)
    }
    return u, nil
}

Migrações com golang-migrate

Para projetos PostgreSQL em produção, golang-migrate é a ferramenta mais usada:

go get -tags 'postgres' github.com/golang-migrate/migrate/v4
go get github.com/golang-migrate/migrate/v4/database/pgx/v5
go get github.com/golang-migrate/migrate/v4/source/file
migrations/
├── 000001_criar_usuarios.up.sql
├── 000001_criar_usuarios.down.sql
├── 000002_criar_posts.up.sql
└── 000002_criar_posts.down.sql
import (
    "github.com/golang-migrate/migrate/v4"
    _ "github.com/golang-migrate/migrate/v4/database/pgx/v5"
    _ "github.com/golang-migrate/migrate/v4/source/file"
)

func executarMigracoes(dsn string) error {
    m, err := migrate.New("file://migrations", dsn)
    if err != nil {
        return fmt.Errorf("criar migrator: %w", err)
    }
    defer m.Close()

    if err := m.Up(); err != nil && err != migrate.ErrNoChange {
        return fmt.Errorf("executar migrações: %w", err)
    }

    versao, _, _ := m.Version()
    log.Printf("Banco na versão %d", versao)
    return nil
}

Resumo do que foi coberto

Este artigo apresentou PostgreSQL com Go usando o driver pgx em profundidade: configuração do pool com pgxpool, parâmetros nomeados, tipos nativos do PostgreSQL, JSONB para documentos semi-estruturados, pgx.CollectRows para scan automático em structs, full-text search com tsvector e ts_rank, transações, Listen/Notify para eventos em tempo real, protocolo COPY para inserção em massa, tratamento de erros específicos do PostgreSQL e migrações com golang-migrate. O próximo artigo explora MongoDB com o driver oficial Go.


Referências e leituras complementares

  • pgx — GitHub — Repositório oficial com documentação completa. https://github.com/jackc/pgx

  • Documentação pgxpool — Configuração e uso do pool de conexões. https://pkg.go.dev/github.com/jackc/pgx/v5/pgxpool

  • Documentação PostgreSQL 16 — Referência completa do PostgreSQL. https://www.postgresql.org/docs/16/

  • golang-migrate — Ferramenta de migrações compatível com PostgreSQL. https://github.com/golang-migrate/migrate

  • PostGIS — Extensão geoespacial para PostgreSQL. https://postgis.net

  • pgvector — Extensão para busca vetorial com embeddings de AI. https://github.com/pgvector/pgvector

Comentários

Mais em Golang

Artigo 27 — Trabalhando com JSON: encode, decode e tags de struct
Artigo 27 — Trabalhando com JSON: encode, decode e tags de struct

Artigo 27 — Trabalhando com JSON: encode, decode e tags de struct JSON: a lín...

Select, timeouts e cancelamento de operações
Select, timeouts e cancelamento de operações

O artigo anterior introduziu o select como mecanismo para aguardar múltiplos...

Artigo 29 — Roteamento avançado com Gorilla Mux ou Chi
Artigo 29 — Roteamento avançado com Gorilla Mux ou Chi

Artigo 29 — Roteamento avançado com Gorilla Mux ou Chi Quando a biblioteca pa...