Ответ
Транзакции в Go используются для выполнения группы SQL-операций как единого, атомарного действия. Это гарантирует, что либо все операции будут успешно выполнены, либо ни одна из них не будет применена (принцип "всё или ничего").
Работа с транзакциями в пакете database/sql
включает следующие шаги:
- Начало транзакции: Создается объект транзакции
*sql.Tx
с помощью методаdb.Begin()
или, что предпочтительнее,db.BeginTx()
для работы с контекстом. - Выполнение операций: Все SQL-запросы (SELECT, INSERT, UPDATE, DELETE) выполняются с использованием методов объекта
tx
, а неdb
(tx.ExecContext()
,tx.QueryRowContext()
, и т.д.). - Завершение транзакции:
tx.Commit()
: Если все операции прошли без ошибок, транзакция фиксируется, и изменения сохраняются в базе данных.tx.Rollback()
: Если на каком-либо шаге произошла ошибка, транзакция откатывается, и все сделанные в ее рамках изменения отменяются.
Пример с лучшими практиками (Best Practices)
Ключевой практикой является использование defer tx.Rollback()
. Это гарантирует, что транзакция будет отменена, если функция завершится с ошибкой до вызова Commit()
.
func transferMoney(ctx context.Context, db *sql.DB, fromID, toID int, amount int) error {
// 1. Начинаем транзакцию с контекстом
tx, err := db.BeginTx(ctx, nil) // Используем nil для опций по умолчанию
if err != nil {
return fmt.Errorf("не удалось начать транзакцию: %w", err)
}
// 2. Гарантируем откат транзакции в случае любой ошибки
// Если Commit() будет выполнен успешно, Rollback() не сделает ничего и не вернет ошибку.
defer tx.Rollback()
// 3. Выполняем операции в рамках транзакции
// Снимаем деньги со счета отправителя
_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromID)
if err != nil {
return fmt.Errorf("ошибка списания: %w", err)
}
// Зачисляем деньги на счет получателя
_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, toID)
if err != nil {
return fmt.Errorf("ошибка зачисления: %w", err)
}
// 4. Если все успешно, фиксируем транзакцию
if err := tx.Commit(); err != nil {
return fmt.Errorf("не удалось зафиксировать транзакцию: %w", err)
}
return nil
}
Ключевые моменты:
- Используйте
BeginTx
: Всегда передавайтеcontext.Context
для управления таймаутами и отменой операций. defer tx.Rollback()
: Это надежный паттерн для избежания "подвисших" транзакций при ошибках.- Обработка ошибок: Тщательно проверяйте ошибку после каждого шага, включая
tx.Commit()
.