Ответ
Для работы с транзакциями в Go используется объект *sql.Tx, который создается из *sql.DB.
Основной флоу работы с транзакцией:
- Начать транзакцию с помощью
db.Begin()илиdb.BeginTx(). - Выполнять все запросы (SELECT, INSERT, UPDATE, DELETE) через объект транзакции
tx, а не черезdb. - Если любая из операций завершилась ошибкой, откатить транзакцию с помощью
tx.Rollback(). - Если все операции успешны, зафиксировать изменения с помощью
tx.Commit().
Пример с использованием defer для надежности:
Этот паттерн гарантирует, что транзакция будет отменена, если что-то пойдет не так.
db, err := sql.Open("postgres", "user=... password=... dbname=... sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 1. Начинаем транзакцию
ctx := context.Background()
tx, err := db.BeginTx(ctx, nil) // Используем BeginTx для передачи контекста
if err != nil {
log.Fatal(err)
}
// 2. Откладываем Rollback. Если Commit выполнится успешно,
// Rollback вернет ошибку sql.ErrTxDone, которую можно безопасно игнорировать.
defer tx.Rollback()
// 3. Выполняем операции внутри транзакции
_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance - 100 WHERE id = 1")
if err != nil {
// Ошибка уже обработается в defer tx.Rollback()
log.Printf("Failed to debit account: %v", err)
return
}
_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance + 100 WHERE id = 2")
if err != nil {
log.Printf("Failed to credit account: %v", err)
return
}
// 4. Если все хорошо, коммитим транзакцию
if err = tx.Commit(); err != nil {
log.Fatal(err)
}
fmt.Println("Transaction committed successfully")
Важные моменты:
- Изоляция: Для установки уровня изоляции транзакции (например,
sql.LevelSerializable) используйтеdb.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable}). - Контекст: Передавайте
context.ContextвBeginTxиExecContext/QueryRowContextдля управления временем выполнения и отменой запросов. - Всегда используйте
tx: Послеdb.Begin(), все операции должны выполняться через объектtx, чтобы они были частью одной транзакции.
Ответ 18+ 🔞
А, смотри-ка, транзакции в Go! Ну это, блядь, как в банке: либо всё прошло, либо нихуя не прошло. Представь, что ты переводишь бабки с одного счёта на другой. Нельзя же, чтобы с одного списали, а на второй не зачислили — это ж пиздец, народ бунтовать начнёт.
Вот, значит, главный герой у нас — *sql.Tx. Рождается он из обычной *sql.DB, как я из бутылки водки после тяжёлого дня.
Как с этим чудом работать, чтобы не обосраться:
- Начинаем дело. Зовём
db.Begin()илиdb.BeginTx(). Это как сказать: "Так, народ, все операции — в одну кучу, будем считать их одной неделимой хуйнёй". - Работаем ТОЛЬКО через него. Получили объект
tx— и все запросы (SELECT,INSERT,UPDATE) делаем через него. Если вдруг начнёшь тыкать в исходныйdb— это уже будет отдельная операция, и вся твоя транзакция нахуй не нужна. Запомнил? Запомни, блядь. - Если пиздец случился — откатываемся.
tx.Rollback()— наша кнопка "отмена". Всё, что наделали внутри транзакции, стирается, как твои обещания бросить пить. - Если всё заебок — фиксируем.
tx.Commit()— это приказ: "Всё, что натворили, записываем на века в базу". После этого точка, возврата нет.
Пример с defer — чтоб наверняка
Тут мы используем defer, это как поставить охранника у двери, который в любом случае, даже если ты сдохнешь на полпути, сделает откат. Умно, да?
db, err := sql.Open("postgres", "user=... password=... dbname=... sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 1. Запускаем движок транзакции
ctx := context.Background()
tx, err := db.BeginTx(ctx, nil) // BeginTx — чтобы контекст прикрутить
if err != nil {
log.Fatal(err)
}
// 2. Ставим охранника-откатчика. Если в конце всё ок и будет Commit,
// этот Rollback вернёт ошибку sql.ErrTxDone, но нам на неё похуй.
defer tx.Rollback()
// 3. Делаем свои тёмные делишки внутри транзакции
_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance - 100 WHERE id = 1")
if err != nil {
// Ошибка — охранник (defer tx.Rollback()) уже всё откатит
log.Printf("Не списали сотку с первого счёта: %v", err)
return
}
_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance + 100 WHERE id = 2")
if err != nil {
log.Printf("Не зачислили сотку на второй счёт: %v", err)
return
}
// 4. Если дошли сюда — значит, всё чики-пуки. Коммитим!
if err = tx.Commit(); err != nil {
log.Fatal(err)
}
fmt.Println("Транзакция прошла, все довольны, можно и выпить")
А теперь, блядь, важные детали, на которых все погорают:
- Изоляция, ёпта. Хочешь, чтобы твою транзакцию никто не видел, пока она не завершится? Используй
db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable}). Это как самый строгий уровень, чтоб никаких гонок данных, блядь. - Контекст — твой друг. Тыкай
context.Contextвезде, куда можно (BeginTx,ExecContext). Он поможет, если запрос завис — просто отменишь его по таймауту, а не будешь ждать до второго пришествия. - ПОВТОРЯЮ, КАК МАНТРУ: После
BeginTxВСЁ делаем черезtx.ExecContextилиtx.QueryRowContext. Если сделаешь черезdb.Exec— это будет отдельный запрос, и он НЕ откатится, если основная транзакция накроется. Пиздец тебе тогда, а не атомарность.
Вот и вся магия. Не так страшен чёрт, как его малюют. Главное — не путать, через какой объект работаешь, а то будет тебе ебаный стыд перед базой данных.