database/sql: pool, context и ошибки
database/sql задаёт переносимый интерфейс к SQL-драйверу. *sql.DB не является соединением: это concurrent-safe пул, который открывает и переиспользует соединения по необходимости. Драйвер импортируется отдельно; его особенности по placeholder, isolation и отмене нельзя скрывать общим API.
Зачем это на интервью
E4 должен не допустить SQL injection, зависший запрос и утечку Rows. E5 должен объяснить, почему пул вызывает очередь, как настроить lifetime, как провести транзакцию и почему retry transaction требует понимания конкретной СУБД.
Минимум для E4
- Вызвать
sql.Open, затем проверить доступность черезPingContextс timeout. - Создать один
*sql.DBна datasource и закрыть его при shutdown. - Использовать
QueryContext,QueryRowContext,ExecContextс context запроса. - Закрыть
rows, вызватьrows.Err()после цикла и проверятьScan. - Передавать значения параметрами, а не склеивать SQL из пользовательской строки.
Углубление для E5/Senior
SetMaxOpenConns ограничивает число соединений; при исчерпании запрос ждёт, поэтому слишком малый предел повышает latency, а слишком большой перегружает БД. SetMaxIdleConns удерживает warm connections. SetConnMaxLifetime помогает переживать лимиты/балансировщики, но одновременная ротация создаёт reconnect storm; добавляйте запас и наблюдайте DB.Stats(). SetConnMaxIdleTime освобождает давно idle connections.
Транзакция использует одно соединение. Все запросы должны идти через tx, а defer tx.Rollback() безопасен: после успешного Commit он вернёт sql.ErrTxDone. Не вызывайте db.QueryContext внутри логической транзакции — это другое соединение и другая атомарность. Контекст отменяет ожидание и, если драйвер поддерживает, запрос; не гарантирует, что сервер БД не успел применить side effect.
Ключевые понятия
package users
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
)
type User struct { ID int64; Email string }
type Repository struct { db *sql.DB }
func Open(ctx context.Context, driver, dsn string) (*Repository, error) {
db, err := sql.Open(driver, dsn)
if err != nil { return nil, err }
db.SetMaxOpenConns(20)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(30 * time.Minute)
db.SetConnMaxIdleTime(5 * time.Minute)
if err := db.PingContext(ctx); err != nil { db.Close(); return nil, fmt.Errorf("ping database: %w", err) }
return &Repository{db: db}, nil
}
func (r *Repository) Find(ctx context.Context, id int64) (User, error) {
var u User
err := r.db.QueryRowContext(ctx, `SELECT id, email FROM users WHERE id = $1`, id).Scan(&u.ID, &u.Email)
if errors.Is(err, sql.ErrNoRows) { return User{}, fmt.Errorf("user %d: %w", id, err) }
if err != nil { return User{}, fmt.Errorf("select user: %w", err) }
return u, nil
}
func (r *Repository) Close() error { return r.db.Close() }Placeholder $1 характерен для PostgreSQL, но database/sql не нормализует placeholder между драйверами. Контракт repository должен отделять sql.ErrNoRows или превращать его в доменную ошибку у границы приложения.
Типовые вопросы
- Что делает
sql.Open?- Валидирует/создаёт handle через зарегистрированный driver; не обязан открывать TCP-соединение. Проверка доступности —
PingContext.
- Валидирует/создаёт handle через зарегистрированный driver; не обязан открывать TCP-соединение. Проверка доступности —
- Почему
rows.Err()послеNext?- Некоторые ошибки чтения появляются во время итерации, когда
Nextуже завершил цикл.
- Некоторые ошибки чтения появляются во время итерации, когда
- Можно ли несколько goroutine на
*sql.DB?- Да, DB safe for concurrent use.
*sql.Txне следует использовать concurrent-но, если конкретный сценарий/driver не документирует это явно.
- Да, DB safe for concurrent use.
- Почему нельзя сделать
defer rows.Close()в длинном цикле запросов?- Закрытие отложится до выхода из функции и может удержать connection из pool. Нужна функция-итерация или явное
Close.
- Закрытие отложится до выхода из функции и может удержать connection из pool. Нужна функция-итерация или явное
- Что возвращает
QueryRowContextпри отсутствии строки?- Сам
QueryRowContextвозвращает placeholder;sql.ErrNoRowsпоявляется приScan.
- Сам
Практика
- Напишите repository
FindиList. Критерий готовности: параметризация есть во всех запросах,Listзакрывает rows и проверяетrows.Err(). - Настройте pool и экспортируйте
DB.Stats(). Критерий готовности: нагрузочный тест демонстрирует ожидание приMaxOpenConns=1, а отчёт содержитWaitCountиWaitDuration. - Оберните перевод денег в транзакцию. Критерий готовности: обе записи используют
tx, rollback выполняется при любой ошибке, test фиксирует отсутствие частичного обновления.
Частые ошибки и ловушки
- Держать глобальную транзакцию или соединение для всего приложения.
- Увеличивать
MaxOpenConns, не проверив лимиты БД и число реплик. - Игнорировать
context.Canceled/DeadlineExceededи повторять операцию без понимания результата. - Вставлять имя колонки/table из пользователя параметром: placeholders работают для значений, не идентификаторов.
- Скрывать ошибку
Closeу rows, когда driver может сообщить deferred error.
Связанные темы
Практический Go · PostgreSQL · Логи, конфигурация и тесты