sqlc для Go: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

sqlc для Go: генерация type-safe кода из SQL

sqlc — это компилятор, который генерирует Go-код на основе ваших SQL-запросов. Вместо того чтобы писать вручную функции для работы с базой данных, вы пишете чистый SQL, а sqlc создаёт за вас типизированные, безопасные и производительные функции. Это принципиально иной подход по сравнению с ORM: вы сохраняете полный контроль над SQL, но избавляетесь от рутинного написания boilerplate-кода.

Зачем это нужно? Во-первых, sqlc гарантирует type-safety на уровне компиляции. Если вы измените схему таблицы или запрос, sqlc перегенерирует код, и компилятор Go сразу укажет на все места, где нужно внести правки. Во-вторых, производительность: сгенерированный код не использует рефлексию и не делает лишних аллокаций, что делает его практически таким же быстрым, как ручное написание запросов через database/sql. В-третьих, это упрощает ревью кода: SQL-запросы явно видны в файлах .sql, а не спрятаны в методах ORM.

sqlc идеально подходит для проектов, где важна производительность и контроль над запросами: высоконагруженные API, финансовые системы, аналитические платформы. Он также отлично сочетается с миграциями (например, golang-migrate) и не привязывает вас к конкретной СУБД — поддерживаются PostgreSQL, MySQL, SQLite.

Установка

sqlc — это не библиотека, а утилита командной строки. Установить её можно несколькими способами:

# Через go install (рекомендуется)
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest

# Через Homebrew (macOS/Linux)
brew install sqlc

# Скачать бинарник с GitHub
# https://github.com/sqlc-dev/sqlc/releases

После установки проверьте версию:

sqlc version

Быстрый старт

Создадим минимальный проект, который демонстрирует работу sqlc.

1. Структура проекта

mkdir sqlc-demo && cd sqlc-demo
go mod init sqlc-demo

2. Конфигурация sqlc

Создайте файл sqlc.yaml в корне проекта:

version: "2"
sql:
  - engine: "postgresql"
    queries: "queries.sql"
    schema: "schema.sql"
    gen:
      go:
        package: "db"
        out: "db"
        sql_package: "pgx/v5"

3. Схема базы данных

Файл schema.sql:

CREATE TABLE authors (
    id   BIGSERIAL PRIMARY KEY,
    name text NOT NULL,
    bio  text
);

4. SQL-запросы

Файл queries.sql:

-- name: GetAuthor :one
SELECT * FROM authors
WHERE id = $1;

-- name: ListAuthors :many
SELECT * FROM authors
ORDER BY name;

-- name: CreateAuthor :one
INSERT INTO authors (name, bio)
VALUES ($1, $2)
RETURNING *;

-- name: DeleteAuthor :exec
DELETE FROM authors
WHERE id = $1;

5. Генерация кода

sqlc generate

После выполнения в папке db/ появятся сгенерированные файлы. Теперь можно использовать их в коде:

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/jackc/pgx/v5/pgxpool"
    "sqlc-demo/db"
)

func main() {
    ctx := context.Background()
    pool, err := pgxpool.New(ctx, "postgres://user:pass@localhost:5432/testdb")
    if err != nil {
        log.Fatal(err)
    }
    defer pool.Close()

    queries := db.New(pool)

    // Создаём автора
    author, err := queries.CreateAuthor(ctx, db.CreateAuthorParams{
        Name: "Brian Kernighan",
        Bio:  "Co-author of The C Programming Language",
    })
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Created author: %+v\n", author)

    // Получаем автора по ID
    fetched, err := queries.GetAuthor(ctx, author.ID)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Fetched author: %+v\n", fetched)

    // Список всех авторов
    authors, err := queries.ListAuthors(ctx)
    if err != nil {
        log.Fatal(err)
    }
    for _, a := range authors {
        fmt.Printf("Author: %s\n", a.Name)
    }
}

Основные методы и функции

sqlc генерирует код на основе аннотаций в SQL-комментариях. Рассмотрим все возможные аннотации и сгенерированные методы.

1. :one — получение одной записи

Сигнатура: func (q *Queries) MethodName(ctx context.Context, arg Params) (Model, error)

Что делает: Выполняет запрос и возвращает ровно одну запись. Если запрос не вернул строк, возвращает ошибку sql.ErrNoRows.

-- name: GetAuthor :one
SELECT * FROM authors WHERE id = $1;
author, err := queries.GetAuthor(ctx, 1)
// author.ID, author.Name, author.Bio

2. :many — получение множества записей

Сигнатура: func (q *Queries) MethodName(ctx context.Context, arg Params) ([]Model, error)

Что делает: Выполняет запрос и возвращает срез записей. Если запрос не вернул строк, возвращается пустой срез (не nil).

-- name: ListAuthors :many
SELECT * FROM authors ORDER BY name;
authors, err := queries.ListAuthors(ctx)
for _, a := range authors {
    fmt.Println(a.Name)
}

3. :exec — выполнение без возврата строк

Сигнатура: func (q *Queries) MethodName(ctx context.Context, arg Params) error

Что делает: Выполняет запрос, не возвращая строки. Используется для INSERT, UPDATE, DELETE без RETURNING.

-- name: DeleteAuthor :exec
DELETE FROM authors WHERE id = $1;
err := queries.DeleteAuthor(ctx, 1)

4. :execrows — выполнение с возвратом количества затронутых строк

Сигнатура: func (q *Queries) MethodName(ctx context.Context, arg Params) (int64, error)

Что делает: Выполняет запрос и возвращает количество строк, которые были изменены/удалены.

-- name: UpdateAuthorBio :execrows
UPDATE authors SET bio = $2 WHERE id = $1;
rowsAffected, err := queries.UpdateAuthorBio(ctx, db.UpdateAuthorBioParams{
    ID:  1,
    Bio: "New bio",
})
fmt.Printf("Updated %d rows\n", rowsAffected)

5. :execresult — выполнение с возвратом результата

Сигнатура: func (q *Queries) MethodName(ctx context.Context, arg Params) (pgconn.CommandTag, error)

Что делает: Выполняет запрос и возвращает pgconn.CommandTag, который содержит метаданные о выполненной команде (например, количество затронутых строк, OID).

-- name: DeleteAllAuthors :execresult
DELETE FROM authors;
result, err := queries.DeleteAllAuthors(ctx)
fmt.Printf("Rows deleted: %d\n", result.RowsAffected())

6. :copyfrom — массовая вставка через COPY

Сигнатура: func (q *Queries) MethodName(ctx context.Context, arg []Params) (int64, error)

Что делает: Использует PostgreSQL COPY для массовой вставки данных. Значительно быстрее, чем множественные INSERT.

-- name: CreateAuthors :copyfrom
INSERT INTO authors (name, bio) VALUES ($1, $2);
authors := []db.CreateAuthorsParams{
    {Name: "Alice", Bio: "Writer"},
    {Name: "Bob", Bio: "Engineer"},
}
count, err := queries.CreateAuthors(ctx, authors)
fmt.Printf("Inserted %d authors\n", count)

7. :batchmany и :batchone — пакетные запросы

Сигнатура: func (q *Queries) MethodName(ctx context.Context, arg []Params) *pgx.BatchResults

Что делает: Выполняет несколько одинаковых запросов в одном пакете. :batchmany для запросов, возвращающих множество строк, :batchone — для одной строки.

-- name: GetAuthorsBatch :batchmany
SELECT * FROM authors WHERE id = $1;
ids := []int32{1, 2, 3}
batch := queries.GetAuthorsBatch(ctx, ids)
for batch.Next() {
    author, err := batch.Values()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Author: %+v\n", author)
}

8. New() — создание экземпляра Queries

Сигнатура: func New(db DBTX) *Queries

Что делает: Создаёт новый экземпляр структуры Queries, которая содержит все сгенерированные методы. Принимает интерфейс DBTX, который реализуют *sql.DB, *sql.Tx, *pgxpool.Pool и другие.

// С pgxpool
pool, _ := pgxpool.New(ctx, connString)
queries := db.New(pool)

// С database/sql
sqlDB, _ := sql.Open("postgres", connString)
queries := db.New(sqlDB)

// С транзакцией
tx, _ := pool.Begin(ctx)
queries := db.New(tx)

9. WithTx() — создание Queries с транзакцией

Сигнатура: func (q *Queries) WithTx(tx *sql.Tx) *Queries (для database/sql) или func (q *Queries) WithTx(tx pgx.Tx) *Queries (для pgx)

Что делает: Создаёт новый экземпляр Queries, привязанный к переданной транзакции. Позволяет выполнять запросы в рамках одной транзакции.

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

qtx := queries.WithTx(tx)
qtx.CreateAuthor(ctx, db.CreateAuthorParams{Name: "Test"})
qtx.DeleteAuthor(ctx, 1)

tx.Commit(ctx)

Реальный практический пример

Создадим полноценный REST API для управления блогом с авторами и постами.

Схема базы данных

-- schema.sql
CREATE TABLE authors (
    id        BIGSERIAL PRIMARY KEY,
    name      VARCHAR(255) NOT NULL,
    email     VARCHAR(255) UNIQUE NOT NULL,
    bio       TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE posts (
    id         BIGSERIAL PRIMARY KEY,
    author_id  BIGINT NOT NULL REFERENCES authors(id),
    title      VARCHAR(255) NOT NULL,
    content    TEXT NOT NULL,
    published  BOOLEAN NOT NULL DEFAULT FALSE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

SQL-запросы

-- queries.sql
-- Authors
-- name: CreateAuthor :one
INSERT INTO authors (name, email, bio)
VALUES ($1, $2, $3)
RETURNING *;

-- name: GetAuthor :one
SELECT * FROM authors WHERE id = $1;

-- name: ListAuthors :many
SELECT * FROM authors ORDER BY created_at DESC;

-- name: UpdateAuthor :one
UPDATE authors
SET name = COALESCE($2, name),
    email = COALESCE($3, email),
    bio = COALESCE($4, bio)
WHERE id = $1
RETURNING *;

-- name: DeleteAuthor :exec
DELETE FROM authors WHERE id = $1;

-- Posts
-- name: CreatePost :one
INSERT INTO posts (author_id, title, content, published)
VALUES ($1, $2, $3, $4)
RETURNING *;

-- name: GetPost :one
SELECT * FROM posts WHERE id = $1;

-- name: ListPostsByAuthor :many
SELECT * FROM posts
WHERE author_id = $1
ORDER BY created_at DESC;

-- name: ListPublishedPosts :many
SELECT p.*, a.name as author_name
FROM posts p
JOIN authors a ON a.id = p.author_id
WHERE p.published = TRUE
ORDER BY p.created_at DESC;

-- name: UpdatePost :one
UPDATE posts
SET title = COALESCE($2, title),
    content = COALESCE($3, content),
    published = COALESCE($4, published),
    updated_at = NOW()
WHERE id = $1
RETURNING *;

-- name: DeletePost :exec
DELETE FROM posts WHERE id = $1;

Конфигурация sqlc

# sqlc.yaml
version: "2"
sql:
  - engine: "postgresql"
    queries: "queries.sql"
    schema: "schema.sql"
    gen:
      go:
        package: "db"
        out: "db"
        sql_package: "pgx/v5"                    

Рекомендаций