Go Admin

O que Todo Dev Deve Saber sobre SQLC em Go: Gerando Código Tipado a partir de Queries SQL Já leu

O que é SQLC e Por que Você Deveria Usar SQLC é uma ferramenta que gera código Go tipado automaticamente a partir de suas queries SQL. Em vez de escrever manualmente structs, funções de mapeamento e tratamento de erros, você define suas queries SQL e o SQLC cuida de gerar todo o código repetitivo de forma segura e eficiente. Isso reduz bugs, melhora a manutenibilidade e elimina a famosa "magia" das ORMs tradicionais. A principal vantagem é a segurança de tipo em tempo de compilação. Se você refatorar uma coluna no banco de dados ou mudar o tipo de um parâmetro, o SQLC vai gerar código que não compila até que você atualize suas queries — nada de descobrir erros em produção. Além disso, SQLC não interpreta SQL em tempo de execução como ORMs fazem; gera código Go puro, resultando em performance superior e sem overhead de reflexão. Por que não apenas usar uma ORM? ORMs como GORM são convenientes,

O que é SQLC e Por que Você Deveria Usar

SQLC é uma ferramenta que gera código Go tipado automaticamente a partir de suas queries SQL. Em vez de escrever manualmente structs, funções de mapeamento e tratamento de erros, você define suas queries SQL e o SQLC cuida de gerar todo o código repetitivo de forma segura e eficiente. Isso reduz bugs, melhora a manutenibilidade e elimina a famosa "magia" das ORMs tradicionais.

A principal vantagem é a segurança de tipo em tempo de compilação. Se você refatorar uma coluna no banco de dados ou mudar o tipo de um parâmetro, o SQLC vai gerar código que não compila até que você atualize suas queries — nada de descobrir erros em produção. Além disso, SQLC não interpreta SQL em tempo de execução como ORMs fazem; gera código Go puro, resultando em performance superior e sem overhead de reflexão.

Por que não apenas usar uma ORM?

ORMs como GORM são convenientes, mas abstraem SQL demais. Você perde controle fino sobre suas queries, performance fica imprevisível, e aprender a "linguagem" da ORM adiciona complexidade. SQLC pega o melhor dos dois mundos: você escreve SQL direto (controle total) e recebe código Go seguro e tipado automaticamente.

Instalação e Configuração Inicial

Instalando o SQLC

Primeiro, instale o SQLC em sua máquina. Visite https://docs.sqlc.dev/en/latest/overview/install.html para o método mais recente. Para a maioria dos usuários:

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

Verifique se está funcionando:

sqlc version

Configurando seu projeto

Crie um arquivo sqlc.yaml na raiz do seu projeto:

version: "2"
sql:
  - engine: "postgres"
    queries: "./queries"
    schema: "./schema"
    gen:
      go:
        out: "./internal/db"
        package: "db"

Esta configuração diz ao SQLC: leia as migrations em ./schema, leia as queries em ./queries e gere código Go em ./internal/db com o package db. Crie os diretórios necessários:

mkdir -p queries schema internal/db

Esquema do banco de dados

Crie schema/001_initial.sql com uma estrutura de exemplo:

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE posts (
    id SERIAL PRIMARY KEY,
    user_id INT NOT NULL REFERENCES users(id),
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Este é apenas um exemplo de schema que usaremos nas próximas seções.

Escrevendo Queries e Gerando Código

Estruturando suas queries

Crie queries/users.sql. SQLC usa comentários especiais para identificar tipos de operação:

-- name: GetUser :one
SELECT id, name, email, created_at FROM users WHERE id = $1;

-- name: ListUsers :many
SELECT id, name, email, created_at FROM users ORDER BY created_at DESC;

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

-- name: DeleteUser :exec
DELETE FROM users WHERE id = $1;

-- name: UpdateUser :exec
UPDATE users SET name = $2, email = $3 WHERE id = $1;

Os comentários especiais definem:
- -- name: NomeDaFuncao: o nome da função que será gerada
- :one: retorna uma única linha (um struct)
- :many: retorna múltiplas linhas (slice de structs)
- :exec: apenas executa (DELETE, UPDATE sem RETURNING)

Crie também queries/posts.sql:

-- name: CreatePost :one
INSERT INTO posts (user_id, title, content) VALUES ($1, $2, $3)
RETURNING id, user_id, title, content, created_at;

-- name: GetPostsByUserID :many
SELECT id, user_id, title, content, created_at FROM posts 
WHERE user_id = $1 ORDER BY created_at DESC;

-- name: GetPost :one
SELECT id, user_id, title, content, created_at FROM posts WHERE id = $1;

Gerando o código

Execute:

sqlc generate

Se tudo estiver correto, você verá novos arquivos em internal/db/. Examine-os:

// internal/db/models.go
package db

import (
    "time"
)

type User struct {
    ID        int32
    Name      string
    Email     string
    CreatedAt time.Time
}

type Post struct {
    ID        int32
    UserID    int32
    Title     string
    Content   string
    CreatedAt time.Time
}
// internal/db/users.sql.go (parcial)
package db

import (
    "context"
)

const getUser = `-- name: GetUser :one
SELECT id, name, email, created_at FROM users WHERE id = $1
`

func (q *Queries) GetUser(ctx context.Context, id int32) (User, error) {
    row := q.db.QueryRowContext(ctx, getUser, id)
    var i User
    err := row.Scan(&i.ID, &i.Name, &i.Email, &i.CreatedAt)
    return i, err
}

Perceba: código seguro, tipado e sem reflexão. Os parâmetros e retornos são verificados em tempo de compilação.

Usando SQLC em uma Aplicação Real

Configurando a conexão com o banco

Crie main.go:

package main

import (
    "context"
    "database/sql"
    "fmt"
    "log"

    _ "github.com/lib/pq"
    "yourmodule/internal/db"
)

func main() {
    // Substitua pelos seus dados de conexão
    connStr := "postgres://user:password@localhost:5432/yourdb?sslmode=disable"

    sqldb, err := sql.Open("postgres", connStr)
    if err != nil {
        log.Fatal(err)
    }
    defer sqldb.Close()

    // Testa a conexão
    if err := sqldb.Ping(); err != nil {
        log.Fatal(err)
    }

    // Cria a instância de Queries
    queries := db.New(sqldb)

    // Seu código aqui
    runExamples(context.Background(), queries)
}

func runExamples(ctx context.Context, q *db.Queries) {
    // Criar um usuário
    user, err := q.CreateUser(ctx, db.CreateUserParams{
        Name:  "João Silva",
        Email: "joao@example.com",
    })
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Usuário criado: %d - %s\n", user.ID, user.Name)

    // Buscar um usuário
    fetchedUser, err := q.GetUser(ctx, user.ID)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Usuário encontrado: %s (%s)\n", fetchedUser.Name, fetchedUser.Email)

    // Listar todos os usuários
    users, err := q.ListUsers(ctx)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Total de usuários: %d\n", len(users))
    for _, u := range users {
        fmt.Printf("  - %s: %s\n", u.Name, u.Email)
    }

    // Criar um post
    post, err := q.CreatePost(ctx, db.CreatePostParams{
        UserID:  user.ID,
        Title:   "Meu Primeiro Post",
        Content: "Este é o conteúdo do meu primeiro post com SQLC!",
    })
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Post criado: %d\n", post.ID)

    // Buscar posts do usuário
    posts, err := q.GetPostsByUserID(ctx, user.ID)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Posts do usuário: %d\n", len(posts))
    for _, p := range posts {
        fmt.Printf("  - %s\n", p.Title)
    }
}

Tratamento de erros com SQLC

SQLC retorna erros padrão do Go. Para diferenciar entre "não encontrado" e outros erros:

import "database/sql"

user, err := q.GetUser(ctx, 999) // ID que não existe
if err != nil {
    if err == sql.ErrNoRows {
        fmt.Println("Usuário não encontrado")
    } else {
        log.Fatal(err)
    }
}

Recursos Avançados e Boas Práticas

Queries parametrizadas com IN

Para queries com múltiplos IDs, SQLC suporta slices:

-- name: GetUsersByIDs :many
SELECT id, name, email, created_at FROM users WHERE id = ANY($1);

Chamado assim:

users, err := q.GetUsersByIDs(ctx, []int32{1, 2, 3})

Transações

SQLC funciona perfeitamente com transações padrão do Go:

tx, err := sqldb.BeginTx(ctx, nil)
if err != nil {
    log.Fatal(err)
}
defer tx.Rollback()

// Cria uma instância de Queries com a transação
qtx := q.WithTx(tx)

user, err := qtx.CreateUser(ctx, db.CreateUserParams{
    Name:  "Maria",
    Email: "maria@example.com",
})
if err != nil {
    return err
}

post, err := qtx.CreatePost(ctx, db.CreatePostParams{
    UserID:  user.ID,
    Title:   "Post dentro de transação",
    Content: "Conteúdo",
})
if err != nil {
    return err
}

return tx.Commit()

Estruturando seu projeto

Uma estrutura bem organizada com SQLC:

projeto/
├── main.go
├── sqlc.yaml
├── go.mod
├── go.sum
├── schema/
│   └── 001_initial.sql
├── queries/
│   ├── users.sql
│   └── posts.sql
└── internal/
    ├── db/
    │   ├── models.go
    │   ├── users.sql.go
    │   └── posts.sql.go
    └── handlers/
        └── user_handler.go

Mantenha suas queries organizadas por domínio e não misture lógica Go com lógica SQL.

Conclusão

SQLC resolve um dos maiores problemas na programação Go: gerar código de acesso a dados que é seguro, eficiente e sem boilerplate. Você ganhou três aprendizados principais neste artigo: (1) SQLC gera código tipado a partir de SQL, eliminando erros de mapeamento em tempo de compilação; (2) o fluxo é simples — defina schema, escreva queries com comentários especiais, execute sqlc generate; (3) o código gerado é Go puro, não há abstrações mágicas, resultando em performance previsível e fácil debugging.

Use SQLC quando você quer controle fino sobre SQL, performance máxima e segurança de tipo. Não use quando precisar de migrations automáticas ou quando trabalhar com vários bancos de dados diferentes na mesma aplicação (embora seja possível com configuração extra).

Referências


Artigos relacionados