ORMs como GORM abstraem o SQL, mas cobram um preço: queries imprevisíveis, performance opaca e tipos genéricos que escondem erros até o runtime. O sqlc inverte essa equação — você escreve SQL puro, e ele gera código Go type-safe com structs, funções e interfaces prontas para uso. Sem reflexão, sem tags mágicas, sem surpresas.
Se você já trabalha com PostgreSQL em Go ou está acostumado a escrever queries manuais com database/sql, o sqlc vai se encaixar naturalmente no seu workflow.
Por que sqlc?
O sqlc resolve um problema real: manter SQL e código Go sincronizados. Em vez de escrever queries como strings dentro do Go (propenso a erros) ou usar um ORM que gera SQL imprevisível, o sqlc valida suas queries contra o schema do banco em tempo de compilação e gera código Go idiomático.
Benefícios concretos:
- Type safety: parâmetros e retornos são structs Go tipados — erros de tipo são pegos pelo compilador
- Zero overhead: o código gerado usa
database/sqloupgxdiretamente, sem camada de abstração - SQL puro: você tem controle total sobre as queries, índices e otimizações
- Validação em build time:
sqlc verifydetecta incompatibilidades entre queries e schema antes do deploy
Instalando o sqlc
# Via Go install
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
# Ou via Homebrew (macOS/Linux)
brew install sqlc
Verifique a instalação:
sqlc version
# v1.27.0
Estrutura do Projeto
Um projeto típico com sqlc segue esta organização:
meu-projeto/
├── sqlc.yaml # Configuração do sqlc
├── sql/
│ ├── schema.sql # DDL — criação de tabelas
│ └── queries.sql # Queries com anotações sqlc
├── internal/
│ └── db/ # Código gerado pelo sqlc
│ ├── db.go
│ ├── models.go
│ └── queries.sql.go
├── main.go
└── go.mod
Configurando o sqlc.yaml
A configuração define onde estão o schema e as queries, qual driver usar e onde gerar o código:
version: "2"
sql:
- engine: "postgresql"
queries: "sql/queries.sql"
schema: "sql/schema.sql"
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
emit_empty_slices: true
O sql_package: "pgx/v5" usa o driver pgx, que é mais performático que database/sql para PostgreSQL. O emit_interface: true gera uma interface Querier que facilita mocking nos testes.
Definindo o Schema
Crie o arquivo sql/schema.sql com a estrutura das tabelas:
CREATE TABLE usuarios (
id BIGSERIAL PRIMARY KEY,
nome VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
senha_hash VARCHAR(255) NOT NULL,
ativo BOOLEAN NOT NULL DEFAULT true,
criado_em TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
atualizado_em TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
titulo VARCHAR(500) NOT NULL,
slug VARCHAR(500) NOT NULL UNIQUE,
conteudo TEXT NOT NULL,
publicado BOOLEAN NOT NULL DEFAULT false,
autor_id BIGINT NOT NULL REFERENCES usuarios(id),
criado_em TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_posts_autor ON posts(autor_id);
CREATE INDEX idx_posts_publicado ON posts(publicado) WHERE publicado = true;
Escrevendo Queries com Anotações
As queries usam comentários especiais que dizem ao sqlc o nome da função Go e o tipo de retorno:
-- name: CriarUsuario :one
INSERT INTO usuarios (nome, email, senha_hash)
VALUES ($1, $2, $3)
RETURNING *;
-- name: BuscarUsuarioPorEmail :one
SELECT * FROM usuarios
WHERE email = $1 AND ativo = true;
-- name: BuscarUsuarioPorID :one
SELECT * FROM usuarios
WHERE id = $1;
-- name: ListarUsuarios :many
SELECT * FROM usuarios
WHERE ativo = true
ORDER BY criado_em DESC
LIMIT $1 OFFSET $2;
-- name: AtualizarUsuario :one
UPDATE usuarios
SET nome = $1, email = $2, atualizado_em = NOW()
WHERE id = $3
RETURNING *;
-- name: DesativarUsuario :exec
UPDATE usuarios
SET ativo = false, atualizado_em = NOW()
WHERE id = $1;
-- name: ContarUsuariosAtivos :one
SELECT COUNT(*) FROM usuarios WHERE ativo = true;
-- name: CriarPost :one
INSERT INTO posts (titulo, slug, conteudo, publicado, autor_id)
VALUES ($1, $2, $3, $4, $5)
RETURNING *;
-- name: ListarPostsPublicados :many
SELECT p.*, u.nome as autor_nome
FROM posts p
JOIN usuarios u ON u.id = p.autor_id
WHERE p.publicado = true
ORDER BY p.criado_em DESC
LIMIT $1;
-- name: BuscarPostPorSlug :one
SELECT p.*, u.nome as autor_nome
FROM posts p
JOIN usuarios u ON u.id = p.autor_id
WHERE p.slug = $1;
As anotações :one, :many e :exec controlam o tipo de retorno:
:one— retorna uma struct ou erro (incluisql.ErrNoRowsse não encontrar):many— retorna um slice de structs:exec— retorna apenas erro (para INSERT/UPDATE/DELETE sem RETURNING)
Gerando o Código
sqlc generate
O sqlc analisa o schema, valida as queries e gera os arquivos Go. O models.go gerado contém as structs:
// internal/db/models.go (gerado automaticamente)
package db
import (
"time"
)
type Usuario struct {
ID int64 `json:"id"`
Nome string `json:"nome"`
Email string `json:"email"`
SenhaHash string `json:"senha_hash"`
Ativo bool `json:"ativo"`
CriadoEm time.Time `json:"criado_em"`
AtualizadoEm time.Time `json:"atualizado_em"`
}
type Post struct {
ID int64 `json:"id"`
Titulo string `json:"titulo"`
Slug string `json:"slug"`
Conteudo string `json:"conteudo"`
Publicado bool `json:"publicado"`
AutorID int64 `json:"autor_id"`
CriadoEm time.Time `json:"criado_em"`
}
E o queries.sql.go contém as funções tipadas:
// internal/db/queries.sql.go (gerado automaticamente)
func (q *Queries) CriarUsuario(ctx context.Context, arg CriarUsuarioParams) (Usuario, error) {
row := q.db.QueryRow(ctx, criarUsuario, arg.Nome, arg.Email, arg.SenhaHash)
var i Usuario
err := row.Scan(
&i.ID, &i.Nome, &i.Email, &i.SenhaHash,
&i.Ativo, &i.CriadoEm, &i.AtualizadoEm,
)
return i, err
}
type CriarUsuarioParams struct {
Nome string `json:"nome"`
Email string `json:"email"`
SenhaHash string `json:"senha_hash"`
}
Note que os parâmetros são structs tipados — não interface{} ou map[string]any. Se você mudar o schema e esquecer de atualizar uma query, sqlc generate aponta o erro.
Usando o Código Gerado
No seu main.go, conecte ao banco e use as queries geradas:
package main
import (
"context"
"fmt"
"log"
"github.com/jackc/pgx/v5/pgxpool"
"meu-projeto/internal/db"
)
func main() {
ctx := context.Background()
pool, err := pgxpool.New(ctx, "postgres://user:pass@localhost:5432/meubanco?sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer pool.Close()
queries := db.New(pool)
// Criar usuário — type-safe, sem strings SQL no código Go
usuario, err := queries.CriarUsuario(ctx, db.CriarUsuarioParams{
Nome: "Maria Silva",
Email: "maria@exemplo.com",
SenhaHash: "$2a$10$hash-aqui",
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Usuário criado: %d - %s\n", usuario.ID, usuario.Nome)
// Listar usuários — retorna []Usuario tipado
usuarios, err := queries.ListarUsuarios(ctx, db.ListarUsuariosParams{
Limit: 10,
Offset: 0,
})
if err != nil {
log.Fatal(err)
}
for _, u := range usuarios {
fmt.Printf(" %s (%s)\n", u.Nome, u.Email)
}
// Buscar post por slug — retorna struct com JOIN
post, err := queries.BuscarPostPorSlug(ctx, "meu-primeiro-post")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Post: %s por %s\n", post.Titulo, post.AutorNome)
}
Tudo é tipado. Se você tentar passar um int onde deveria ser string, o compilador avisa. Se a query retorna colunas que não existem no schema, sqlc generate falha. Zero surpresas em runtime.
Transactions com sqlc
Para operações que precisam de atomicidade, o sqlc suporta transactions de forma idiomática:
func criarUsuarioComPost(ctx context.Context, pool *pgxpool.Pool) error {
tx, err := pool.Begin(ctx)
if err != nil {
return fmt.Errorf("erro ao iniciar transação: %w", err)
}
defer tx.Rollback(ctx)
queries := db.New(tx)
usuario, err := queries.CriarUsuario(ctx, db.CriarUsuarioParams{
Nome: "João Santos",
Email: "joao@exemplo.com",
SenhaHash: "$2a$10$hash-aqui",
})
if err != nil {
return fmt.Errorf("erro ao criar usuário: %w", err)
}
_, err = queries.CriarPost(ctx, db.CriarPostParams{
Titulo: "Meu Primeiro Post",
Slug: "meu-primeiro-post",
Conteudo: "Conteúdo do post...",
Publicado: true,
AutorID: usuario.ID,
})
if err != nil {
return fmt.Errorf("erro ao criar post: %w", err)
}
return tx.Commit(ctx)
}
O db.New() aceita tanto *pgxpool.Pool quanto pgx.Tx, graças à interface DBTX gerada pelo sqlc. Esse padrão de tratamento de erros com wrapping usando %w facilita o debugging quando algo falha.
sqlc verify: Validação em CI
O comando sqlc verify compara suas queries contra o schema e detecta problemas antes do deploy. Adicione ao seu pipeline de CI:
sqlc verify
Se alguém altera uma coluna no schema sem atualizar as queries correspondentes, o verify falha com uma mensagem clara. Isso é especialmente valioso em equipes grandes onde schema e queries podem ser alterados por pessoas diferentes. Para quem já usa TDD e CI/CD com Go, o sqlc verify se encaixa naturalmente no pipeline.
sqlc vs GORM vs database/sql Puro
| Aspecto | sqlc | GORM | database/sql |
|---|---|---|---|
| SQL | Você escreve | ORM gera | Você escreve |
| Type safety | Compilação | Runtime | Nenhum |
| Performance | Máxima | Overhead de reflexão | Máxima |
| Curva de aprendizado | SQL + anotações | API do ORM | SQL + boilerplate |
| Migrations | Externo (goose, migrate) | Built-in | Externo |
| Queries complexas | SQL nativo — sem limites | Limitado pelo ORM | SQL nativo |
| Validação | Build time | Runtime | Runtime |
O sqlc brilha quando: você quer performance máxima, precisa de queries complexas (CTEs, window functions, JOINs múltiplos), ou quer que o compilador pegue erros de tipo no acesso ao banco.
GORM é mais conveniente para CRUD simples e prototipagem rápida. Para projetos Go em produção com queries não-triviais, sqlc é a escolha mais idiomática.
Próximos Passos
O sqlc transforma a forma como você interage com bancos de dados em Go. Comece com um CRUD simples, valide o workflow de schema → queries → generate, e expanda para transactions e queries mais complexas.
Para monitorar a performance das suas queries em produção, combine com OpenTelemetry para tracing de banco de dados. Se você está construindo uma API REST ou microsserviços, sqlc com pgx é uma das combinações mais performáticas do ecossistema Go.
Para projetos que também envolvem clean architecture, a interface Querier gerada pelo sqlc facilita a inversão de dependência — seu domínio depende da interface, não da implementação concreta.
Se você trabalha com múltiplas linguagens, veja como Python lida com acesso a banco de dados com SQLAlchemy para comparar abordagens.
FAQ
O sqlc suporta MySQL?
Sim. O sqlc suporta PostgreSQL, MySQL e SQLite. A configuração muda o engine no sqlc.yaml e o driver Go correspondente.
Preciso de um banco rodando para gerar o código?
Não. O sqlc analisa o schema SQL estaticamente. Você não precisa de um banco de dados rodando para sqlc generate — ele parseia o SQL localmente.
Como faço migrations com sqlc? O sqlc não gerencia migrations. Use ferramentas como goose, golang-migrate ou atlas para controlar versões do schema. O sqlc consome o schema final — não importa como ele chegou lá.
O código gerado é editável?
O código gerado não deve ser editado manualmente — ele é sobrescrito a cada sqlc generate. Se precisa de lógica adicional, crie funções wrapper no seu código que chamam as funções geradas.
sqlc funciona com pgx/v5?
Sim, e é a combinação recomendada. Configure sql_package: "pgx/v5" no sqlc.yaml. O pgx oferece melhor performance que database/sql para PostgreSQL, com suporte nativo a tipos como pgtype.Timestamptz.