Go: database/sql и ORM

10 вопросов

1 Пакет database/sql в Go. Основные типы.

database/sql - универсальный интерфейс к SQL-БД. sql.DB - пул соединений, не привязан к конкретной БД (драйвер регистрируется через init). Методы: Query, QueryRow, Exec, Begin, BeginTx. Контекст передается в QueryContext, ExecContext для таймаута и отмены. Результат - sql.Rows (итерация) или sql.Row (одна строка), сканирование через Scan.

import _ "github.com/lib/pq"
db, _ := sql.Open("postgres", connStr)
row := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = $1", id)
var name string
row.Scan(&name)
Открыть отдельно →
2 Пул соединений sql.DB в Go. Настройка.

sql.DB управляет пулом соединений. SetMaxOpenConns(n) - макс. открытых соединений; SetMaxIdleConns(n) - макс. в пуле простоя; SetConnMaxLifetime(d) - время жизни соединения. Рекомендуется задавать под лимиты БД и нагрузку. Не открывать новую DB на каждый запрос - один экземпляр на приложение. Закрытие: db.Close() при shutdown.

db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)
Открыть отдельно →
3 Prepared statements в database/sql. Кеширование.

Prepare возвращает sql.Stmt; при повторных вызовах один запрос разбирается один раз. DB кеширует подготовленные запросы по строке; при Prepare драйвер может вернуть закешированный. В высоконагруженных сценариях явный Stmt может быть полезен; обычно достаточно Query/Exec с параметрами - драйвер сам кеширует. Stmt нужно закрывать (Stmt.Close) при освобождении.

stmt, _ := db.PrepareContext(ctx, "SELECT name FROM users WHERE id = $1")
defer stmt.Close()
row := stmt.QueryRowContext(ctx, id)
Открыть отдельно →
4 Транзакции в database/sql. BeginTx и откат.

BeginTx(ctx, opts) начинает транзакцию с опциональным уровнем изоляции. Полученный sql.Tx используется для Query/Exec/Prepare в рамках транзакции. Обязательно вызывать Rollback при ошибке (через defer), иначе соединение не вернется в пул. Commit после успешных операций.

tx, err := db.BeginTx(ctx, nil)
if err != nil { return err }
defer tx.Rollback()
_, err = tx.ExecContext(ctx, "INSERT ...")
if err != nil { return err }
return tx.Commit()
Открыть отдельно →
5 pgx и database/sql. Когда использовать pgx напрямую.

database/sql - общий интерфейс; драйвер pgx реализует его. Для только Postgres можно использовать pgx напрямую (github.com/jackc/pgx/v5): нативный протокол, быстрее, дополнительные возможности (batch, copy, уведомления, типы). Миграция с database/sql на pgxpool минимальна: замена sql.DB на pgxpool.Pool, вызовы похожи (QueryRow, Exec). Для мульти-БД остаются database/sql.

pool, _ := pgxpool.New(ctx, connStr)
row := pool.QueryRow(ctx, "SELECT ...", id)
pool.Close()
Открыть отдельно →
6 sqlx в Go. Преимущества над database/sql.

sqlx расширяет database/sql: привязка к структурам (Get, Select), именованные запросы (:name), срезы структур. Меньше ручного Scan в цикле. Совместим с sql.DB - оборачивает его. Используют для удобства без полноценного ORM. В новых проектах часто выбирают pgx или чистый database/sql с небольшими хелперами.

var users []User
err := db.Select(&users, "SELECT * FROM users WHERE active = $1", true)
var u User
err := db.Get(&u, "SELECT * FROM users WHERE id = $1", id)
Открыть отдельно →
7 GORM в Go. Плюсы и минусы.

Плюсы: удобный API (Create, Find, Where, Preload), миграции, хуки, связи. Минусы: магия, скрытые запросы (N+1 при неверном Preload), сложнее отлаживать производительность. Для сложных запросов и высокой нагрузки часто предпочитают сырой SQL или sqlx. GORM подходит для быстрой разработки CRUD и простых моделей.

db.Create(&User{Name: "Alice"})
var u User
db.Preload("Orders").First(&u, id)
Открыть отдельно →
8 sql.Null типы и указатели в Go при сканировании.

NULL в БД нужно сканировать в тип, допускающий отсутствие значения: sql.NullString, sql.NullInt64, sql.NullTime и т.д., или указатель *string, *int. NullString имеет Valid и String; при Valid=true использовать String. Указатели проще в коде (nil при NULL); при маршалинге в JSON nil даст null. Выбор по удобству и соглашениям проекта.

var name sql.NullString
row.Scan(&name)
if name.Valid { use(name.String) }
// или
var name *string
row.Scan(&name)
if name != nil { use(*name) }
Открыть отдельно →
9 Контекст в запросах к БД в Go. Таймаут и отмена.

Все методы принимают context.Context: QueryContext, ExecContext, BeginTx. При отмене контекста (таймаут, cancel) драйвер должен прервать запрос и освободить ресурсы. В HTTP handler передают r.Context(); при долгих операциях создают контекст с таймаутом. Важно не держать контекст запроса для фоновых задач - создавать свой контекст.

ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
row := db.QueryRowContext(ctx, "SELECT ...", id)
Открыть отдельно →
10 Жизненный цикл соединения и пула в Go. Graceful shutdown.

При старте приложения открывают sql.DB (или pgxpool.Pool) один раз, передают в handlers/repositories. При shutdown вызывают db.Close() - ожидает завершения активных запросов и закрывает пул. Чтобы не принимать новые запросы при shutdown, отменяют контекст сервера (Shutdown) и ждут завершения обработчиков; затем закрывают DB. Таймаут на shutdown ограничивает время ожидания.

srv.Shutdown(ctx)
db.Close()
Открыть отдельно →
🧠Квиз 🏆Лидеры 🎯Собесед. 📖Вопросы 📚База зн.