Все главы учебника
Содержание учебника
Глава 10 / Базы данных

Go: параметры запросов и пул соединений

16 мин чтенияКонтент v0.10.0

Обработчик сайта получает номер читателя из URL и показывает его активные выдачи. Пока запросы запускал человек в psql, он сам выбирал значения. Теперь входные данные приходят от незнакомого клиента. Одновременно сайт обслуживает несколько запросов, поэтому появляется ещё одна задача: ограничить количество соединений с базой и не держать их дольше необходимого. Разберём эти две обязанности отдельно, затем соберём безопасный запрос.

Значения передаются отдельно от SQL

Запрос для Анны уже знаком. В psql можно проверить его буквально:

SELECT l.id, b.title
FROM club.loans AS l
JOIN club.copies AS c ON c.id = l.copy_id
JOIN club.books AS b ON b.id = c.book_id
WHERE l.member_id = 1 AND l.returned_on IS NULL
ORDER BY l.id;

Ожидаются строки 1000 и 1002, обе с названием «Практика Go». Для Веры результат пустой. Пустой список выдач ещё не доказывает отсутствие читателя: для ответа HTTP 404 потребуется отдельно проверить members. Это часть контракта обработчика, которую база не угадывает.

Нельзя подставлять ввод через fmt.Sprintf в текст запроса. Строка пользователя способна изменить смысл SQL, если становится его частью. Параметр оставляет структуру команды постоянной: PostgreSQL получает значение отдельно. В PostgreSQL параметры обозначаются $1, $2; знак вопроса не является универсальным синтаксисом всех драйверов.

rows, err := db.QueryContext(ctx, `
 SELECT id FROM club.loans
 WHERE member_id = $1 AND returned_on IS NULL
 ORDER BY id`, memberID)

memberID здесь — разобранное целое число, а db — открытый *sql.DB. Даже после проверки типа продолжайте использовать параметры. Проверка ввода отвечает за допустимый диапазон и понятную ошибку клиенту, параметры — за отделение данных от команды. Ни один из этих механизмов не заменяет авторизацию: читатель не получает чужую историю только потому, что передал корректное число.

Имя колонки или направление сортировки нельзя передать как обычное значение $1. Если API допускает выбор сортировки, сопоставьте разрешённые варианты с заранее написанными SQL-фрагментами. Не добавляйте произвольную строку пользователя после ORDER BY. Например, oldest выбирает ORDER BY borrowed_on, id, а newest — ORDER BY borrowed_on DESC, id DESC; неизвестный вариант отклоняется.

Драйвер и жизненный цикл

Стандартный пакет database/sql задаёт общий интерфейс, но не содержит драйвер PostgreSQL. Приложению нужен отдельный драйвер, например адаптер stdlib проекта pgx. Подключение стороннего модуля делайте через Go modules с зафиксированной версией и go.sum. Документация выбранной версии определяет поддерживаемые версии Go и особенности отмены запросов. Установить пакет недостаточно: сервер PostgreSQL должен работать отдельно.

sql.Open создаёт дескриптор пула; он не гарантирует успешного сетевого подключения. Проверка при старте выполняется через PingContext с ограничением времени. Строку соединения берите из конфигурации или переменной окружения. Не печатайте её целиком в лог: там может находиться пароль. Один *sql.DB обычно живёт весь срок работы процесса и безопасен для использования несколькими goroutine. Создание нового пула на каждый HTTP-запрос увеличивает число соединений и усложняет управление ими.

После успешного QueryContext сразу поставьте defer rows.Close(). Затем читайте строки через rows.Next(), проверяйте каждую ошибку Scan и после цикла — rows.Err(). Ошибка может возникнуть не только в момент отправки запроса, но и при получении следующих строк. Пока Rows не закрыт, соединение может оставаться занятым. Для nullable email выбирайте sql.NullString либо явно определённое представление NULL; Scan в обычную строку не превращает отсутствие значения в пустой адрес автоматически.

Ограниченный ресурс

Пул повторно использует соединения и регулирует их выдачу обработчикам. SetMaxOpenConns ограничивает открытые соединения, SetMaxIdleConns — оставленные без работы. Это разные числа. Таймаут контекста ограничивает ожидание и исполнение запроса, а серверный statement_timeout ограничивает выполнение на стороне PostgreSQL. При отмене всегда проверяйте поведение драйвера и полученную ошибку.

Если три экземпляра сервиса допускают по 20 соединений, потенциальный бюджет сервиса равен 60. К нему добавляются миграции, администрирование и другие приложения. Увеличение лимита не гарантирует роста производительности: одновременно исполняемые запросы конкурируют за CPU, память и диски. Начальные числа выбирают по бюджету сервера, затем проверяют измерением нагрузки. db.Stats() показывает использование пула и ожидания; сравнивайте изменения WaitCount и WaitDuration за интервал, а не только накопленное значение.

Транзакция занимает одно соединение до Commit или Rollback. Все её запросы выполняйте через tx, а не через db: иначе часть команд уйдёт вне транзакции. При лимите пула один открытый tx и попытка получить ещё одно соединение через db могут заставить обработчик ждать самого себя. Отложенный Rollback после BeginTx полезен для ранних выходов; успешный Commit проверяйте отдельно. Не держите транзакцию во время долгого HTTP-запроса к другому сервису.

Самостоятельная лаборатория

Отведите 45–60 минут. Напишите небольшую Go-команду, принимающую номер читателя, подключающуюся к вашей новой учебной базе и печатающую активные loan id. Используйте database/sql, выбранный PostgreSQL-драйвер, параметры, контекст и полный цикл обработки Rows. Проверьте читателей 1 и 3; затем остановите только собственный учебный сервер и убедитесь, что команда сообщает ошибку подключения за ограниченное время.

Критерии готовности: для Анны выводятся 1000 и 1002, для Веры — пустой список; ввод не склеивается с SQL; пароль не появляется в сообщении; ошибка Scan и rows.Err не теряются. Добавьте отдельный запрос email Бориса и корректно распознайте NULL. После возвращения сервера приложение снова работает без изменения seed.

Полный необязательный разбор находится в go-client архива лабораторий. В нём есть go.mod/go.sum и читающая команда: go run . -member 2 выводит Бориса с email=NULL. DB_LAB_URL задаётся заранее. Можно сначала написать собственную версию, затем сравнить закрытие Rows, проверку ошибок и обработку отсутствующего читателя. Короткий фрагмент выше показывает вызов внутри программы; самостоятельной программой он не является.

Разбор

Параметр защищает структуру SQL, но не бизнес-права. Пул ограничивает соединения, но не исправляет медленный запрос. Контекст ограничивает ожидание, но таймаут ответа при записи не является доказательством отсутствия Commit. Для команды чтения это проще: повтор безопасен. Для будущей выдачи понадобятся инвариант уникальной активной записи и определённое поведение повторного запроса. Если приложение увеличится до нескольких экземпляров, пересчитайте общий бюджет соединений до изменения локального лимита.

Первичные источники

Go: защита от SQL injection, чтение строк, управление соединениями, pgx и адаптер database/sql.