Опишите процесс работы с SQL-транзакциями в Go

Ответ

Транзакции в Go используются для выполнения группы SQL-операций как единого, атомарного действия. Это гарантирует, что либо все операции будут успешно выполнены, либо ни одна из них не будет применена (принцип "всё или ничего").

Работа с транзакциями в пакете database/sql включает следующие шаги:

  1. Начало транзакции: Создается объект транзакции *sql.Tx с помощью метода db.Begin() или, что предпочтительнее, db.BeginTx() для работы с контекстом.
  2. Выполнение операций: Все SQL-запросы (SELECT, INSERT, UPDATE, DELETE) выполняются с использованием методов объекта tx, а не db (tx.ExecContext(), tx.QueryRowContext(), и т.д.).
  3. Завершение транзакции:
    • 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().