Как управлять транзакциями в базе данных с помощью Go?

Ответ

Управление транзакциями в Go через стандартный пакет database/sql включает три ключевых действия: начало транзакции, фиксацию (commit) и откат (rollback).

Основной механизм

  1. db.Begin() или db.BeginTx(ctx, opts): Начинает новую транзакцию и возвращает объект *sql.Tx.
  2. tx.Commit(): Фиксирует все изменения, сделанные в рамках транзакции. Если до этого момента произошла ошибка, вызов Commit() также вернет ошибку.
  3. tx.Rollback(): Откатывает все изменения, сделанные в транзакции. Этот метод безопасно вызывать несколько раз или после Commit() (он просто ничего не сделает).

Пример кода (Best Practice)

Самый надежный способ — использовать defer с Rollback(), чтобы гарантировать откат в случае паники или любой ошибки.

func updateUser(db *sql.DB, id int, newName string) error {
    // 1. Начинаем транзакцию
    tx, err := db.Begin()
    if err != nil {
        return fmt.Errorf("could not begin transaction: %w", err)
    }
    // 2. Гарантируем откат, если Commit не будет вызван успешно
    // Эта переменная поможет избежать отката уже успешной транзакции
    committed := false
    defer func() {
        if !committed {
            tx.Rollback() // Откат, если что-то пошло не так
        }
    }()

    // 3. Выполняем операции внутри транзакции
    _, err = tx.Exec("UPDATE users SET name = ? WHERE id = ?", newName, id)
    if err != nil {
        return fmt.Errorf("could not update user: %w", err)
    }

    // ... другие операции ...

    // 4. Фиксируем транзакцию
    if err = tx.Commit(); err != nil {
        return fmt.Errorf("could not commit transaction: %w", err)
    }

    committed = true // Сообщаем defer, что все хорошо
    return nil
}

Ключевые моменты:

  • Используйте defer tx.Rollback(): Это защитит от утечки соединений и незавершенных транзакций при ошибках.
  • Не игнорируйте ошибки Commit() и Rollback(): Ошибка при коммите означает, что изменения не были сохранены. Ошибка при откате может сигнализировать о проблемах с соединением.
  • Контекст (context.Context): Для управления таймаутами и отменой используйте db.BeginTx(ctx, nil). Если контекст будет отменен, транзакция автоматически откатится.
  • Оставленная транзакция: Если вы не вызовете ни Commit(), ни Rollback(), транзакция останется "висеть" до закрытия соединения. Это плохая практика, так как она удерживает блокировки в БД и занимает соединение из пула.