← Voltar para o blog

SQLc com Go: Banco de Dados Type-Safe com Geração de Código

Aprenda a usar sqlc em Go para gerar código type-safe a partir de SQL puro. Tutorial completo com CRUD, transactions, batch operations e comparação com GORM.

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/sql ou pgx diretamente, sem camada de abstração
  • SQL puro: você tem controle total sobre as queries, índices e otimizações
  • Validação em build time: sqlc verify detecta 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 (inclui sql.ErrNoRows se 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

AspectosqlcGORMdatabase/sql
SQLVocê escreveORM geraVocê escreve
Type safetyCompilaçãoRuntimeNenhum
PerformanceMáximaOverhead de reflexãoMáxima
Curva de aprendizadoSQL + anotaçõesAPI do ORMSQL + boilerplate
MigrationsExterno (goose, migrate)Built-inExterno
Queries complexasSQL nativo — sem limitesLimitado pelo ORMSQL nativo
ValidaçãoBuild timeRuntimeRuntime

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.