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