---
title: "sqlc em Go: SQL Type-Safe com PostgreSQL"
url: "https://golang.com.br/blog/sqlc-go-postgresql-typesafe/"
markdown_url: "https://golang.com.br/blog/sqlc-go-postgresql-typesafe.MD"
description: "Aprenda sqlc em Go para gerar código type-safe a partir de SQL real: PostgreSQL, pgx, migrations, transações, testes e padrões de produção."
date: "2026-05-26"
author: "Golang Brasil"
---

# sqlc em Go: SQL Type-Safe com PostgreSQL

Aprenda sqlc em Go para gerar código type-safe a partir de SQL real: PostgreSQL, pgx, migrations, transações, testes e padrões de produção.


`sqlc` em Go resolve uma tensão comum em backends: você quer escrever SQL explícito, otimizado e fácil de revisar, mas também quer segurança de tipos, autocomplete, testes melhores e menos código repetitivo no repositório. Em vez de esconder o banco atrás de um ORM pesado, o sqlc lê suas queries SQL e gera código Go com structs, métodos e assinaturas coerentes com o schema.

Essa abordagem combina bem com a cultura Go: ferramentas simples, contratos explícitos e pouco runtime mágico. Você continua controlando índices, joins, transações, `EXPLAIN`, locks e migrations, mas ganha uma camada type-safe para chamar essas queries na aplicação. O resultado é especialmente útil em APIs, workers, backends financeiros, produtos B2B e sistemas que dependem de PostgreSQL de forma séria.

Este guia mostra quando usar sqlc, como organizar queries, como configurar com PostgreSQL e `pgx`, como pensar em transações e quais cuidados evitam dor em produção. Se você ainda está montando a base, leia também [Go com PostgreSQL](/aprenda/golang-postgresql/), [migrations em Go para banco de dados](/blog/migrations-go-banco-dados-producao/) e [context.Context em Go](/blog/context-timeout-cancelamento-go/).

## O problema: SQL solto ou abstração demais

Em muitos projetos Go, o acesso ao banco começa com `database/sql` direto no handler ou no repositório:

```go
row := db.QueryRowContext(ctx, `
    SELECT id, email, name
      FROM users
     WHERE id = $1
`, userID)
```

Isso é honesto e funciona. O problema aparece quando o projeto cresce. Você passa a repetir `Scan`, esquecer ordem de colunas, duplicar structs, descobrir erro de tipo só em runtime e fazer refactors sem apoio do compilador. Uma coluna muda de `TEXT` para `UUID`, uma query deixa de retornar um campo, um `NULL` aparece onde o Go esperava `string`, e a falha só surge no teste ou em produção.

No extremo oposto, alguns times colocam um ORM para evitar repetição. O ORM pode ajudar em CRUD simples, mas também pode esconder queries ruins, dificultar otimização, gerar SQL inesperado e criar uma camada de abstração que não conversa bem com recursos específicos do PostgreSQL.

O sqlc fica no meio: SQL continua sendo SQL. O código Go gerado vira uma fronteira tipada entre aplicação e banco.

## Como o sqlc funciona

O fluxo básico tem três entradas:

1. Arquivos de migration ou schema, com `CREATE TABLE`, índices, enums e constraints.
2. Arquivos `.sql` com queries nomeadas.
3. Um `sqlc.yaml` dizendo qual engine usar e onde gerar o pacote Go.

A partir disso, o sqlc valida as queries contra o schema e gera métodos Go. Uma query chamada `GetUser` vira algo como:

```go
user, err := queries.GetUser(ctx, id)
```

Se a query espera `uuid.UUID`, a assinatura expõe isso. Se retorna colunas nullable, o tipo gerado reflete `pgtype`, `sql.NullString` ou o tipo configurado. Se você remove uma coluna usada por uma query, a geração falha antes do deploy.

Esse feedback curto é o maior ganho. O banco deixa de ser um contrato implícito espalhado em strings e passa a participar do ciclo normal de compilação e revisão.

## Instalação e configuração mínima

Instale a ferramenta:

```bash
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
```

Uma configuração moderna com PostgreSQL e `pgx` pode começar assim:

```yaml
version: "2"
sql:
  - engine: "postgresql"
    schema: "db/migrations"
    queries: "db/queries"
    gen:
      go:
        package: "db"
        out: "internal/db"
        sql_package: "pgx/v5"
        emit_json_tags: true
        emit_prepared_queries: false
        emit_interface: true
        emit_exact_table_names: false
```

O caminho `db/migrations` pode apontar para seus arquivos usados por `golang-migrate`, Atlas, Goose ou outra ferramenta. O importante é que o schema visto pelo sqlc seja o mesmo que chega ao banco de produção. Se migrations e sqlc divergem, você perde parte do benefício.

Para gerar:

```bash
sqlc generate
```

Em CI, rode esse comando e falhe se houver diff não commitado. Assim, query, schema e código gerado andam juntos.

## Exemplo de schema e query

Imagine uma tabela simples de usuários:

```sql
CREATE TABLE users (
    id UUID PRIMARY KEY,
    email TEXT NOT NULL UNIQUE,
    name TEXT NOT NULL,
    active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

Agora crie `db/queries/users.sql`:

```sql
-- name: GetUserByID :one
SELECT id, email, name, active, created_at
  FROM users
 WHERE id = $1;

-- name: CreateUser :one
INSERT INTO users (id, email, name)
VALUES ($1, $2, $3)
RETURNING id, email, name, active, created_at;

-- name: ListActiveUsers :many
SELECT id, email, name, active, created_at
  FROM users
 WHERE active = true
 ORDER BY created_at DESC
 LIMIT $1 OFFSET $2;

-- name: DeactivateUser :exec
UPDATE users
   SET active = false
 WHERE id = $1;
```

Os comentários `-- name:` definem o nome do método e a cardinalidade esperada. `:one` exige uma linha, `:many` retorna slice, `:exec` executa sem mapear retorno. Esse contrato é simples e aparece claramente no review.

No serviço Go, o uso fica próximo do domínio:

```go
func (s *UserService) CreateUser(ctx context.Context, input CreateUserInput) (db.User, error) {
    return s.queries.CreateUser(ctx, db.CreateUserParams{
        ID:    uuid.New(),
        Email: strings.ToLower(input.Email),
        Name:  input.Name,
    })
}
```

Você não escreve `Scan`, não repete struct manualmente e não descobre coluna fora de ordem por acidente.

## Transações sem mágica

Em aplicações reais, uma query isolada raramente basta. Você precisa abrir transação, atualizar domínio, gravar outbox, criar auditoria ou validar consistência. O sqlc não impede isso. Ele gera um tipo `Queries` que pode trabalhar com uma interface compatível com conexão ou transação.

Um padrão comum:

```go
func (s *Service) Transfer(ctx context.Context, input TransferInput) error {
    tx, err := s.pool.Begin(ctx)
    if err != nil {
        return err
    }
    defer tx.Rollback(ctx)

    qtx := s.queries.WithTx(tx)

    if err := qtx.DebitAccount(ctx, db.DebitAccountParams{
        ID:     input.FromAccountID,
        Amount: input.Amount,
    }); err != nil {
        return err
    }

    if err := qtx.CreditAccount(ctx, db.CreditAccountParams{
        ID:     input.ToAccountID,
        Amount: input.Amount,
    }); err != nil {
        return err
    }

    return tx.Commit(ctx)
}
```

A regra continua a mesma de qualquer código Go com banco: use `context.Context`, defina timeout no nível certo, trate `Rollback` como limpeza, mantenha transações curtas e não faça chamada HTTP externa enquanto segura lock. Para eventos, combine com [outbox pattern em Go](/blog/outbox-pattern-go-eventos-confiaveis/), não publique no broker dentro da transação principal.

## Nullable, enums e tipos customizados

O ponto que mais separa exemplo de produção é o tratamento de `NULL`. SQL permite ausência de valor; Go precisa de tipo explícito. Dependendo da configuração, sqlc pode gerar `sql.NullString`, tipos de `pgtype` ou ponteiros. Escolha uma convenção e mantenha em todo o projeto.

Para campos realmente opcionais, `NULL` pode fazer sentido. Para estado de domínio, prefira `NOT NULL` com default e constraints. Um `status TEXT NOT NULL CHECK (...)` costuma ser mais claro que um campo nullable que aceita qualquer string.

Enums também merecem atenção. PostgreSQL enum é rígido e bom para estados estáveis, mas migrations de enum podem ser menos flexíveis. `TEXT` com `CHECK` é mais simples de evoluir em alguns produtos. O sqlc lida com ambos, mas a decisão é de modelagem, não da ferramenta.

Para UUID, timestamps, JSONB e numeric, configure overrides se os tipos padrão não forem os que o time usa. O objetivo é evitar conversões manuais espalhadas nos serviços.

## Organização por domínio

Em projetos pequenos, um pacote `internal/db` com todas as queries funciona. Em sistemas maiores, pense em separação por domínio:

```text
db/
  migrations/
  queries/
    users.sql
    accounts.sql
    invoices.sql
internal/
  db/
  users/
  billing/
```

Evite transformar o pacote gerado em seu domínio. Os tipos gerados representam linhas e parâmetros de queries, não necessariamente entidades ricas. Uma struct `db.User` pode ser suficiente para CRUD simples, mas um fluxo de negócio mais complexo talvez mereça um tipo próprio em `internal/users`.

A fronteira saudável é: sqlc cuida do contrato SQL; o serviço cuida de regra de negócio; o handler cuida de HTTP; o domínio não depende de detalhes acidentais de uma query.

## Testes com sqlc

O sqlc reduz erros de compilação, mas não elimina testes de integração. Você ainda precisa validar migrations, constraints, índices e comportamento real do PostgreSQL. Para queries críticas, rode testes contra um Postgres de verdade em container ou ambiente efêmero.

Um teste útil cria schema limpo, roda migrations, insere dados e chama o método gerado:

```go
func TestCreateUser(t *testing.T) {
    ctx := context.Background()
    pool := testPostgresPool(t)
    queries := db.New(pool)

    user, err := queries.CreateUser(ctx, db.CreateUserParams{
        ID:    uuid.New(),
        Email: "ana@example.com",
        Name:  "Ana",
    })
    require.NoError(t, err)
    require.Equal(t, "ana@example.com", user.Email)
}
```

Para lógica de serviço, você pode usar a interface emitida pelo sqlc e criar fake em testes unitários. Só não use fake como substituto total de teste real de query. O bug mais caro geralmente está na combinação de SQL, dados e constraint.

## Quando sqlc não é a melhor escolha

sqlc não é obrigatório para todo projeto. Se você tem uma ferramenta pequena com duas queries, `database/sql` manual pode ser suficiente. Se o produto exige query builder dinâmico com dezenas de filtros opcionais, você pode combinar sqlc com SQL montado de forma controlada ou usar outra ferramenta em pontos específicos.

Também existe custo operacional: alguém precisa entender migrations, revisar SQL e manter o código gerado atualizado. Em times que querem fingir que banco relacional não existe, sqlc vai parecer mais explícito do que gostariam. Para sistemas Go sérios, isso é uma vantagem.

Use sqlc quando:

- O banco relacional é parte central do produto.
- Você quer SQL revisável e otimizado.
- O time valoriza feedback de compilação.
- Refactors de schema precisam ser seguros.
- Queries críticas merecem testes e contratos claros.

Evite depender só dele quando:

- A maior parte das queries é gerada dinamicamente.
- O schema ainda muda de forma caótica sem migration disciplinada.
- O time não roda CI com geração e testes de banco.

## Checklist de produção

Antes de adotar sqlc em uma API Go, valide estes pontos:

- Migrations são a fonte de verdade do schema.
- `sqlc generate` roda no CI.
- Código gerado é commitado ou gerado de forma reprodutível no build.
- Queries recebem `context.Context` e respeitam timeout.
- Transações são curtas e explícitas.
- `NULL`, UUID, JSONB e timestamps têm convenção definida.
- Testes de integração rodam contra PostgreSQL real.
- Índices acompanham queries de listagem e filtros.
- Erros de constraint são mapeados para respostas de domínio ou HTTP.
- O pacote gerado não vaza regra de negócio para handlers.

## Conclusão

sqlc é uma boa escolha para times Go que gostam de SQL e querem menos surpresa em produção. Ele não tenta transformar banco relacional em objeto mágico. Ele aceita que SQL é uma linguagem poderosa, valida esse SQL contra o schema e gera uma camada Go previsível para o resto da aplicação.

A combinação mais forte é simples: migrations disciplinadas, PostgreSQL bem modelado, queries sqlc pequenas e nomeadas, serviços com `context.Context`, testes de integração e observabilidade. Com isso, você mantém a clareza do SQL sem abrir mão da segurança de tipos que faz Go brilhar em sistemas de backend.

Se você está comparando stacks backend no mercado brasileiro, use este guia junto com [vagas Go no Brasil](/vagas/) e com o material de [entrevista técnica Go](/carreira/entrevista-tecnica-go-2026/). Para acompanhar oportunidades em outras linguagens e entender onde SQL, PostgreSQL e engenharia de dados aparecem fora do ecossistema Go, o portal <a href="https://python.dev.br/vagas/" target="_blank" rel="noopener noreferrer" onclick="umami.track('portfolio-site-click', { destination: 'python.dev.br' })">Python Dev Brasil reúne vagas Python e dados no Brasil</a>.
