5. Использование database/sql с параметрами
Передача параметров в Go: Практика правильного кода
В стандартной библиотеке Go работа с безопасными запросами реализована максимально прозрачно. Вместо того чтобы собирать строку вручную, мы отдаем базу данных шаблон запроса и отдельно — список параметров.
Научимся использовать плейсхолдеры и разберемся, почему синтаксис SQL-параметров отличается в зависимости от выбранной вами СУБД.
1. Плейсхолдеры (Placeholder)
Это специальные символы в тексте SQL-запроса, которые говорят Go-пакету database/sql — "Сюда на этапе выполнения будут подставлены данные".
| СУБД | Плейсхолдеры | Пример запроса в Go |
|---|---|---|
| PostgreSQL | $1, $2, $3 |
WHERE id = $1 AND name = $2 |
| MySQL | ? |
WHERE id = ? AND name = ? |
| SQLite | ? |
WHERE id = ? AND name = ? |
Важный нюанс:
В Go-пакете database/sql вы передаете данные в том же порядке, в котором идут плейсхолдеры.
// Для PostgreSQL
db.Query("SELECT * FROM users WHERE id = $1 AND name = $2", id, name)
// Для MySQL
db.Query("SELECT * FROM users WHERE id = ? AND name = ?", id, name)
2. Различия между Query и Exec
В Go-пакете database/sql есть два основных способа отправки запросов. Оба поддерживают параметры.
- db.Query: Используется для команд, возвращающих данные (
SELECT). Вы получаете объект*sql.Rows, по которому нужно пройтись в цикле. - db.Exec: Используется для команд, изменяющих состояние (
INSERT,UPDATE,DELETE). Возвращаетsql.Result, где можно узнать ID вставленной строки или количество затронутых строк.
И Query, и Exec принимают параметры одним и тем же способом — через ?-плейсхолдер и список аргументов. INSERT-запрос с пользовательскими данными выглядит так:
// ✅ INSERT с параметризацией — даже значение HTTP-заголовка безопасно
query := "INSERT INTO tracking (tracking_id, user_agent) VALUES (?, ?)"
_, err := db.Exec(query, headerValue, "Go-Client")
Тот же приём применим к UPDATE и DELETE — никаких отличий от SELECT в плане безопасности.
3. Шаблон с подстановкой LIKE
Подстановка значений в LIKE-условие новичков часто сбивает: знаки % нельзя оставить внутри SQL-шаблона, иначе ввод пользователя снова вклеится в команду. Правильный приём — добавить % в значение аргумента, а не в саму строку запроса:
// ❌ УЯЗВИМО: % внутри SQL, ввод склеивается со строкой запроса
query := fmt.Sprintf("SELECT id, name FROM products WHERE name LIKE '%%%s%%'", search)
rows, _ := db.Query(query)
// ✅ БЕЗОПАСНО: % добавлены к значению аргумента, шаблон фиксированный
query := "SELECT id, name FROM products WHERE name LIKE ?"
rows, _ := db.Query(query, "%"+search+"%")
Драйвер по-прежнему обрабатывает "%abc%" как обычное строковое значение — даже если search содержит ' UNION SELECT ..., оно интерпретируется как часть подстроки для LIKE-сравнения. Тот же паттерн годится для cookie, заголовков, имени пользователя из БД (Second-Order) — везде, где нужна wildcard-подстрока.
4. Кэширование Prepared Statement в структуре
При частом вызове одного и того же запроса (логин, поиск, applied_coupon) подготовку шаблона можно сделать один раз — в конструкторе хендлера — и переиспользовать *sql.Stmt. План запроса кэшируется в БД, повторные вызовы становятся дешевле.
type Handler struct {
db *sql.DB
loginStmt *sql.Stmt // кэш одного prepared statement
}
func NewHandler(db *sql.DB) (*Handler, error) {
stmt, err := db.Prepare("SELECT id FROM users WHERE username=? AND password=?")
if err != nil {
return nil, fmt.Errorf("prepare login: %w", err)
}
return &Handler{db: db, loginStmt: stmt}, nil
}
// В обработчике:
var id int
err := h.loginStmt.QueryRow(username, password).Scan(&id)
stmt.QueryRow(args...) точно так же разделяет шаблон и данные — но шаблон уже скомпилирован один раз при старте приложения. Альтернатива — локальный db.Prepare + defer stmt.Close() внутри обработчика; для одноразовых запросов проще ограничиться обычным db.QueryRow(query, args...).
5. Таблица типичных ошибок при работе с параметрами
| Ошибочное действие | Почему это плохо? | Исправление |
|---|---|---|
fmt.Sprintf внутри Query |
Инъекция всё еще возможна! | Query("... WHERE id = ?", id) |
| Использование не того плейсхолдера | База вернет синтаксическую ошибку | Используйте $1 для Postgres |
| Перепутан порядок аргументов | Будут подставлены не те данные | Сверяйте порядок $1, $2... |
% внутри SQL-строки в LIKE |
Ввод снова склеивается со строкой | db.Query("... LIKE ?", "%"+v+"%") |
6. Почему параметры в Go — это безопасность?
Параметризация на 100% блокирует возможность внедрения SQL-кода. Хакер может подставить любую строку, но Go-драйвер передает её базе как "литеральное значение". База данных никогда не будет воспринимать данные из параметров как SQL-команду.
Использование плейсхолдеров — это признак профессионального кода. База данных сама позаботится о безопасности, если вы дадите ей такую возможность.
Но почему именно этот метод так эффективен? Пора заглянуть глубже и изучить механику защиты на уровне протокола.
Продолжить чтение
Что бы прочитать модуль полностью, зарегистрируйтесь/войдите на платформу
Когда закончишь — отметь раздел, чтобы продолжить.