Janeiro 2026 · ~14 min

Go com PostgreSQL: conexão e CRUD usando pgx

Conecte Go ao PostgreSQL com pgx/v5: exemplo executável, DATABASE_URL, CRUD, pgxpool, transações e migrations. Resolva erros de conexão com Postgres.

Go com PostgreSQL: conexão e CRUD usando pgx

Como conectar Go ao PostgreSQL? Instale github.com/jackc/pgx/v5, configure a variável DATABASE_URL e execute uma consulta com context.Context. Para uma API ou worker com requisições concorrentes, use pgxpool, compartilhando um pool por processo. Os mantenedores do pgx recomendam sua interface nativa quando a aplicação usa somente PostgreSQL e não precisa de bibliotecas que exigem database/sql.

Este tutorial de Go + PostgreSQL — também chamado de Golang com Postgres — começa com um programa executável e avança para CRUD com placeholders $1, pool, transações e migrations. O pgx é o driver; sqlc pode gerar código que usa esse driver, enquanto GORM oferece uma camada de ORM.

Se você já passou do primeiro QueryRow, consulte pgxpool em produção para métricas e configuração ou transações PostgreSQL em Go para locks, retry e isolamento.

O que você vai construir

EtapaEntrega
SetupPostgres 16 no Docker + módulo Go com pgx
Conexãopgxpool com ping, timeout e config de pool
CRUDCreate / Read / Update / Delete com RETURNING
TransaçõesDébito/crédito atômico com Begin + Rollback
SchemaMigrations versionadas com golang-migrate
DecisãoQuando ficar em pgx, ir para sqlc ou GORM

Ao final você tem um repositório enxuto pronto para embutir numa API REST em Go e empacotar com Docker.

Setup: Postgres local e dependências

Docker para desenvolvimento

docker run --name postgres-go \
  -e POSTGRES_USER=app \
  -e POSTGRES_PASSWORD=senha123 \
  -e POSTGRES_DB=myapp \
  -p 127.0.0.1:5432:5432 \
  -d postgres:16

O exemplo usa Postgres 16 e publica a porta somente em 127.0.0.1. A senha é fictícia e serve apenas para este banco local descartável; não a reutilize em produção. Aguarde o banco ficar pronto:

docker exec postgres-go pg_isready -U app -d myapp

Quando aparecer accepting connections, continue. Em Compose, veja a configuração ao lado da API no guia de Go com Docker.

Instalar pgx

mkdir go-postgres && cd go-postgres
go mod init example.com/go-postgres
go get github.com/jackc/pgx/v5
go get github.com/jackc/pgx/v5/pgxpool

Teste rápido de conexão

No mesmo terminal, defina a URL do banco local:

export DATABASE_URL='postgres://app:senha123@localhost:5432/myapp?sslmode=disable'

sslmode=disable é apenas para este ambiente local. Em produção, use a configuração TLS e a CA exigidas pelo provedor; verify-full verifica também o hostname do servidor (veja a documentação de TLS do PostgreSQL). Não publique arquivos .env com credenciais.

Salve o programa abaixo como main.go. Ele usa uma conexão única e um prazo de cinco segundos para conectar e consultar:

package main

import (
	"context"
	"fmt"
	"os"
	"time"

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

func main() {
	dsn := os.Getenv("DATABASE_URL")
	if dsn == "" {
		panic("defina DATABASE_URL antes de executar")
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	conn, err := pgx.Connect(ctx, dsn)
	if err != nil {
		panic(err)
	}
	defer func() {
		closeCtx, cancel := context.WithTimeout(context.Background(), time.Second)
		defer cancel()
		_ = conn.Close(closeCtx)
	}()

	var version string
	if err := conn.QueryRow(ctx, "select version()").Scan(&version); err != nil {
		panic(err)
	}
	fmt.Println(version)
}

Execute:

go run .

A saída deve incluir a versão do servidor, por exemplo PostgreSQL 16.... Se falhar, consulte a tabela abaixo antes de implementar o CRUD.

Erros comuns ao conectar Go ao Postgres

SintomaO que verificar
connection refusedExecute docker ps e pg_isready; confira se a porta local é 5432 e se o banco terminou de iniciar.
password authentication failedConfira usuário e senha da URL. Em volumes persistentes da imagem oficial, alterar POSTGRES_PASSWORD não redefine a senha de um banco já inicializado.
database "myapp" does not existConfira POSTGRES_DB e o nome do banco no caminho da URL.
Erro de TLS/certificadoUse a CA e o hostname indicados pelo provedor. Não desative TLS para contornar um erro em produção.
context deadline exceededConfira conectividade, carga do banco e prazo da operação; não aumente o timeout sem investigar.

API em outro container? localhost aponta para o próprio container, não para o banco. Em Compose, use o nome do serviço (por exemplo postgres:5432) na URL. Se a senha tiver caracteres reservados como @, : ou /, aplique percent-encoding no componente da senha, conforme a sintaxe de URI do PostgreSQL.

pgx.Connect abre uma conexão única — útil para scripts, CLIs e testes de conexão. Para compartilhar conexões entre requisições, use pgxpool na próxima seção.

pgx ou database/sql? O pgx oferece tipos PostgreSQL, COPY, batch e cache de prepared statements. Quando uma biblioteca exige *sql.DB, use github.com/jackc/pgx/v5/stdlib. lib/pq é outro driver para database/sql; avalie os requisitos e a documentação atual do projeto antes de decidir sobre uma migração.

Conexão com pgxpool (caminho recomendado)

package main

import (
	"context"
	"log"
	"os"
	"time"

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

func main() {
	ctx := context.Background()
	dsn := os.Getenv("DATABASE_URL")
	if dsn == "" {
		log.Fatal("defina DATABASE_URL antes de executar")
	}

	cfg, err := pgxpool.ParseConfig(dsn)
	if err != nil {
		log.Fatal(err)
	}
	cfg.MaxConns = 10
	cfg.MinConns = 2
	cfg.MaxConnLifetime = time.Hour
	cfg.MaxConnIdleTime = 30 * time.Minute
	cfg.HealthCheckPeriod = time.Minute

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

	pingCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
	defer cancel()
	if err := pool.Ping(pingCtx); err != nil {
		log.Fatal(err)
	}

	log.Println("conectado ao PostgreSQL com pgxpool")
	// ... handlers / workers usam o mesmo *pgxpool.Pool
}

Este bloco é uma alternativa ao main.go anterior, não deve ser colado como um segundo main. A criação do pool não garante que o banco esteja acessível: confirme com Ping. Os valores MaxConns=10 e MinConns=2 são exemplos, não uma recomendação universal. Planeje o total de conexões de todas as réplicas dentro do orçamento do banco, reservando espaço para administração e outros clientes.

Regras que evitam dor em produção:

  1. URL via env (DATABASE_URL) — nunca hardcode senha.
  2. Timeout em toda query — context.WithTimeout por request HTTP ou job.
  3. Um pool por processo — compartilhe o *pgxpool.Pool; não abra conexão por request.
  4. Feche no graceful shutdown — veja o padrão de context e timeout em Go e de middleware HTTP.

Detalhes de sizing, saturacão e métricas do pool estão em pgxpool para PostgreSQL em produção.

Modelo e schema inicial

CREATE TABLE IF NOT EXISTS users (
    id         BIGSERIAL PRIMARY KEY,
    name       TEXT        NOT NULL,
    email      TEXT        NOT NULL UNIQUE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
type User struct {
	ID        int64     `json:"id"`
	Name      string    `json:"name"`
	Email     string    `json:"email"`
	CreatedAt time.Time `json:"created_at"`
}

Salve o SQL em schema.sql e aplique no banco local:

docker exec -i postgres-go psql -U app -d myapp < schema.sql

Em app real esse CREATE TABLE vira migration (seção abaixo), não Exec solto no boot — exceto protótipos. Os trechos de CRUD a seguir são funções para um repositório, não programas independentes. Além de User, declare var ErrNotFound = errors.New("usuário não encontrado") e importe context, errors, time, github.com/jackc/pgx/v5 e github.com/jackc/pgx/v5/pgxpool no arquivo que os reúne.

CRUD completo com pgx

Create

func createUser(ctx context.Context, pool *pgxpool.Pool, name, email string) (User, error) {
	const q = `
		INSERT INTO users (name, email)
		VALUES ($1, $2)
		RETURNING id, name, email, created_at`

	var u User
	err := pool.QueryRow(ctx, q, name, email).
		Scan(&u.ID, &u.Name, &u.Email, &u.CreatedAt)
	return u, err
}

RETURNING evita um SELECT extra e é o idioma natural do Postgres com Go.

Read (um e lista)

func getUserByID(ctx context.Context, pool *pgxpool.Pool, id int64) (User, error) {
	const q = `SELECT id, name, email, created_at FROM users WHERE id = $1`

	var u User
	err := pool.QueryRow(ctx, q, id).
		Scan(&u.ID, &u.Name, &u.Email, &u.CreatedAt)
	if errors.Is(err, pgx.ErrNoRows) {
		return User{}, ErrNotFound // erro de domínio, não 500
	}
	return u, err
}

func listUsers(ctx context.Context, pool *pgxpool.Pool) ([]User, error) {
	const q = `SELECT id, name, email, created_at FROM users ORDER BY id`

	rows, err := pool.Query(ctx, q)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var out []User
	for rows.Next() {
		var u User
		if err := rows.Scan(&u.ID, &u.Name, &u.Email, &u.CreatedAt); err != nil {
			return nil, err
		}
		out = append(out, u)
	}
	return out, rows.Err()
}

Sempre defer rows.Close() e cheque rows.Err() — vazar cursor esgota o pool.

Update e Delete

func updateUser(ctx context.Context, pool *pgxpool.Pool, id int64, name, email string) error {
	tag, err := pool.Exec(ctx,
		`UPDATE users SET name = $1, email = $2 WHERE id = $3`,
		name, email, id,
	)
	if err != nil {
		return err
	}
	if tag.RowsAffected() == 0 {
		return ErrNotFound
	}
	return nil
}

func deleteUser(ctx context.Context, pool *pgxpool.Pool, id int64) error {
	tag, err := pool.Exec(ctx, `DELETE FROM users WHERE id = $1`, id)
	if err != nil {
		return err
	}
	if tag.RowsAffected() == 0 {
		return ErrNotFound
	}
	return nil
}

Trate “zero linhas” como 404 de domínio. Para o mapa de erros Go, veja tratamento de erros em Go.

Transações

func transfer(ctx context.Context, pool *pgxpool.Pool, fromID, toID int64, amount int64) error {
	tx, err := pool.Begin(ctx)
	if err != nil {
		return err
	}
	defer tx.Rollback(ctx) // no-op depois do Commit

	if amount <= 0 || fromID == toID {
		return errors.New("transferência inválida")
	}

	tag, err := tx.Exec(ctx,
		`UPDATE accounts SET balance = balance - $1 WHERE id = $2 AND balance >= $1`,
		amount, fromID,
	)
	if err != nil {
		return err
	}
	if tag.RowsAffected() != 1 {
		return errors.New("origem inexistente ou saldo insuficiente")
	}

	tag, err = tx.Exec(ctx,
		`UPDATE accounts SET balance = balance + $1 WHERE id = $2`,
		amount, toID,
	)
	if err != nil {
		return err
	}
	if tag.RowsAffected() != 1 {
		return errors.New("destino inexistente")
	}

	return tx.Commit(ctx)
}

O exemplo pressupõe uma tabela accounts com id único e balance inteiro não nulo (valor em unidades mínimas, como centavos). É uma demonstração de atomicidade, não um sistema financeiro completo: conferir RowsAffected impede creditar sem debitar ou perder o débito quando o destino não existe. Erros antes do commit provocam rollback; o contexto precisa continuar válido para o rollback.

Para SELECT … FOR UPDATE, deadlocks, retry em serialização e isolation levels, o guia dedicado é transações, locks e retry em Go.

Batch e prepared statements

Batch (vários inserts numa ida à rede)

batch := &pgx.Batch{}
batch.Queue(`INSERT INTO users (name, email) VALUES ($1, $2)`, "Alice", "[email protected]")
batch.Queue(`INSERT INTO users (name, email) VALUES ($1, $2)`, "Bob", "[email protected]")

br := pool.SendBatch(ctx, batch)
defer br.Close()

for i := 0; i < 2; i++ {
	if _, err := br.Exec(); err != nil {
		return err
	}
}

Prepared statements

O pgx prepara statements automaticamente no nível da conexão do pool quando a mesma query se repete. Você raramente precisa chamar Prepare à mão em serviços HTTP; foque em SQL estável (sem montar o texto dinamicamente) para o cache de prepared statements funcionar.

Migrations com golang-migrate

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

migrate create -ext sql -dir migrations -seq create_users
# edite migrations/000001_create_users.up.sql e .down.sql

export DATABASE_URL='postgres://app:senha123@localhost:5432/myapp?sslmode=disable'
migrate -path migrations -database "$DATABASE_URL" up

Exemplo de par:

-- 000001_create_users.up.sql
CREATE TABLE users (
    id         BIGSERIAL PRIMARY KEY,
    name       TEXT        NOT NULL,
    email      TEXT        NOT NULL UNIQUE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 000001_create_users.down.sql
DROP TABLE IF EXISTS users;

Boas práticas:

  • Rode up no pipeline de deploy antes de subir pods novos.
  • Nunca edite uma migration já aplicada em produção — crie a próxima.
  • Prefira SQL explícito a AutoMigrate de ORM em serviços críticos.
  • Guia irmão: migrations de banco em Go.

pgx vs database/sql vs sqlc vs GORM

AbordagemQuando usarCusto
pgx diretoControle total, hot path, features PostgresVocê escreve SQL e Scan
database/sql + pgx stdlibPrecisa de interface *sql.DB portávelPerde parte das APIs nativas do pgx
sqlcSQL real + tipos gerados, zero reflexãoWorkflow de geração de código
GORMCRUD rápido, protótipo, admin internoAbstração, N+1 se descuidar

Escolha pelo projeto: pgx dá acesso direto às APIs do driver; sqlc gera código a partir de SQL e pode usar pgx; GORM oferece abstrações de ORM. Compare em profundidade em GORM em produção e sqlc typesafe.

Estrutura de projeto sugerida

project/
├── cmd/api/main.go
├── internal/
│   ├── db/pool.go          # NewPool(cfg) *pgxpool.Pool
│   └── user/
│       ├── model.go
│       └── repository.go   # Create/Get/List/Update/Delete
├── migrations/
│   ├── 000001_create_users.up.sql
│   └── 000001_create_users.down.sql
├── docker-compose.yml
└── go.mod

O repositório recebe *pgxpool.Pool (ou uma interface mínima) e devolve erros de domínio. Handlers HTTP ficam finos — encaixe natural com API REST em Go ou Chi em produção.

Testes de repositório

  1. Integração com Postgres de verdade (testcontainers ou Compose de CI) — preferível a mockar SQL.
  2. Rode migrations no TestMain antes da suíte.
  3. Cada teste abre uma transação e faz ROLLBACK no cleanup, ou usa schema/database isolado.
  4. Cubra ErrNoRows, unique violation (23505) e timeout de context.

O kit geral de testes está em testes em Go; o padrão de tabela e t.Run se aplica igual a repositórios.

Checklist de produção

  1. Placeholders $1…$n — zero concatenação de input.
  2. Context com deadline em toda ida ao banco.
  3. Pool dimensionado por réplica; monitore acquire wait.
  4. rows.Close() + rows.Err() sempre.
  5. Migrations versionadas no deploy.
  6. Erros de domínio (not found, conflict) separados de erros de infra.
  7. TLS conforme o provedor — prefira sslmode=verify-full com CA confiável para verificar o certificado e o hostname; não copie disable para produção.
  8. Observabilidade — trace spans em volta de queries lentas; logs estruturados com slog.
  9. Secrets só em env/secret manager — nunca em imagem Docker.
  10. Graceful shutdown fecha o pool depois de drenar requests.

Armadilhas comuns

  • Abrir sql.Open/pgxpool.New por request — destrói latência e o Postgres.
  • Ignorar pgx.ErrNoRows — vira 500 em vez de 404.
  • Pool gigante em cada pod — 20 pods × 50 conns = 1.000 sessões; o banco morre antes da app.
  • Migration no boot da app com várias réplicas — corrida; rode no job de deploy.
  • Scan de NULL em string/int64 sem pointer/pgtype — use *string ou tipos do pgx.
  • Esquecer índice em email UNIQUE — o UNIQUE já cria índice, mas joins e filtros ad-hoc não.

Próximos passos na trilha

  1. Exponha o repositório numa API REST com validação e middleware.
  2. Ajuste o pool com o guia de pgxpool em produção.
  3. Adicione migrations formais e CI.
  4. Avalie sqlc se o volume de SQL crescer.
  5. Empacote API + Postgres com Docker Compose.
  6. Meça contenda com transações e locks.

Perguntas frequentes

Qual o melhor driver PostgreSQL para Go em 2026?

Para aplicações exclusivamente PostgreSQL, pgx/v5 é a recomendação deste guia, seguindo os critérios dos mantenedores: não depender de bibliotecas que exigem database/sql. Ele oferece pgxpool, tipos nativos, batch e COPY. Se a equipe precisa de *sql.DB, use o adaptador github.com/jackc/pgx/v5/stdlib. Não é preciso migrar um driver existente sem avaliar o benefício.

O que é go pgx / pgx em Golang?

pgx é um driver e toolkit PostgreSQL para Go, instalado como github.com/jackc/pgx/v5. pgx.Connect abre uma conexão; pgxpool gerencia conexões compartilhadas para APIs e workers. O pacote stdlib adapta o driver à interface database/sql.

Como conectar Go ao PostgreSQL com pgxpool?

go get github.com/jackc/pgx/v5/pgxpool, ParseConfig + NewWithConfig (ou New com a URL), Ping com timeout e um único pool compartilhado no processo. Configure MaxConns/MinConns e passe context em toda query.

Como conectar Golang ao PostgreSQL passo a passo?

go mod init, go get do pgx e do pgxpool, Postgres no Docker, pgxpool.New com a DATABASE_URL, Ping com timeout e queries com context + placeholders $1. As seções acima mostram cada passo com código completo — incluindo o teste rápido de conexão logo após a instalação.

Como fazer migrations em Go com PostgreSQL?

Com golang-migrate: arquivos .up.sql / .down.sql versionados, migrate up no deploy e down só em rollback controlado. Evite AutoMigrate de ORM como única estratégia em produção.

Quando usar GORM, sqlc ou pgx puro?

pgx para SQL explícito e APIs nativas do PostgreSQL; sqlc para gerar código Go tipado a partir de SQL; GORM quando a abstração de ORM for adequada ao projeto. sqlc pode gerar código para pgx, portanto não são alternativas mutuamente exclusivas.

Como evitar SQL injection e vazamento de conexão?

Placeholders sempre; nunca fmt.Sprintf na query. defer rows.Close(), context com timeout, pool único e tratamento correto de ErrNoRows.

Qual tamanho de connection pool usar?

Não há tamanho universal. Planeje o orçamento usando max_connections do banco, quantidade de réplicas e outros clientes; reserve conexões administrativas. MaxConns=10 no exemplo demonstra a API. Ajuste com testes de carga, espera por aquisição e carga no PostgreSQL, não apenas pelo número de CPUs.

Referências oficiais

Veja também

Perguntas frequentes

Qual o melhor driver PostgreSQL para Go em 2026?

Para aplicações que usam somente PostgreSQL e não dependem de bibliotecas que exigem database/sql, recomendo pgx/v5. Ele oferece pgxpool, tipos PostgreSQL, COPY e batch. Quando precisar de *sql.DB, use o adaptador github.com/jackc/pgx/v5/stdlib; não é necessário trocar um driver existente sem avaliar os requisitos.

O que é go pgx / pgx em Golang?

pgx é um driver e toolkit PostgreSQL para Go, instalado como github.com/jackc/pgx/v5. pgx.Connect abre uma conexão única; pgxpool gerencia um pool compartilhado para APIs e workers. O pacote stdlib adapta o driver à interface database/sql.

Como conectar Go ao PostgreSQL com pgxpool?

Instale github.com/jackc/pgx/v5/pgxpool, chame pgxpool.New(ctx, databaseURL), faça Ping e configure MaxConns, MinConns e MaxConnLifetime via ParseConfig. Sempre passe context.Context com timeout nas queries e feche o pool no shutdown.

Como conectar Golang ao PostgreSQL passo a passo?

Crie o módulo com go mod init, instale github.com/jackc/pgx/v5 e github.com/jackc/pgx/v5/pgxpool, suba um Postgres local com Docker, abra o pool com pgxpool.New(ctx, os.Getenv("DATABASE_URL")), confirme com Ping e rode queries com context.Context e placeholders $1. O passo a passo completo com CRUD, transações e migrations está neste guia.

Como fazer migrations em Go com PostgreSQL?

Use golang-migrate (CLI ou biblioteca): crie pares .up.sql/.down.sql versionados, rode migrate -path ./migrations -database $DATABASE_URL up em deploy e down em rollback. Em produção, prefira migrations SQL explícitas a AutoMigrate de ORM.

Quando usar GORM, sqlc ou pgx puro?

Use pgx direto para SQL explícito e APIs nativas do PostgreSQL; sqlc para gerar código Go tipado a partir de SQL; GORM quando a abstração de ORM for adequada ao projeto. sqlc pode gerar código para pgx, portanto não são alternativas mutuamente exclusivas.

Como evitar SQL injection e vazamento de conexão em Go?

Use sempre placeholders ($1, $2) — nunca concatene entrada do usuário na query. Feche rows com defer, use context com timeout, configure o pool e trate sql.ErrNoRows / pgx.ErrNoRows como 'não encontrado', não como falha de infraestrutura.

Qual tamanho de connection pool usar com Go e PostgreSQL?

Não há tamanho universal. Defina um orçamento de conexões considerando max_connections do PostgreSQL, réplicas da aplicação e outros clientes. O exemplo usa MaxConns=10 apenas para demonstrar a configuração; ajuste com testes de carga e métricas de espera do pool.