> Как обрабатывать ошибки при отмене брони на складе (Go)
Уровень: senior · Роль: backend · Категория: Технические вопросы
Компании: sferaplatform.ru
Стек: Go
> Пример ответа
При обработке отмены брони на складе в Go важно учитывать несколько ключевых сценариев: бронь уже отменена, товар отгружен, или возникла ошибка базы данных. Рекомендую использовать паттерн "retry" с экспоненциальной задержкой для временных сбоев и чёткое разделение ошибок на бизнес-логику и инфраструктурные.
Пример реализации:
GOtype BookingService struct {db *sql.DB}var (ErrBookingNotFound = errors.New("бронь не найдена")ErrBookingCancelled = errors.New("бронь уже отменена")ErrBookingShipped = errors.New("товар уже отгружен, отмена невозможна")ErrConcurrentModify = errors.New("конфликт при изменении брони"))func (s *BookingService) CancelBooking(ctx context.Context, bookingID int64) error {const maxRetries = 3var lastErr errorfor i := 0; i < maxRetries; i++ {err := s.cancelBookingTx(ctx, bookingID)if err == nil {return nil}// Если ошибка бизнес-логики - не повторяемif errors.Is(err, ErrBookingNotFound) ||errors.Is(err, ErrBookingCancelled) ||errors.Is(err, ErrBookingShipped) {return err}// Если конфликт или временная ошибка - повторяемif errors.Is(err, ErrConcurrentModify) || isTemporaryError(err) {lastErr = errtime.Sleep(time.Duration(100*(1<<i)) * time.Millisecond) // 100ms, 200ms, 400mscontinue}// Неизвестная ошибка - возвращаем сразуreturn fmt.Errorf("отмена брони %d: %w", bookingID, err)}return fmt.Errorf("отмена брони %d после %d попыток: %w", bookingID, maxRetries, lastErr)}func (s *BookingService) cancelBookingTx(ctx context.Context, bookingID int64) error {tx, err := s.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})if err != nil {return fmt.Errorf("начало транзакции: %w", err)}defer tx.Rollback()// Проверяем статус брони с блокировкой строкиvar status stringerr = tx.QueryRowContext(ctx,"SELECT status FROM bookings WHERE id = $1 FOR UPDATE", bookingID).Scan(&status)if err == sql.ErrNoRows {return ErrBookingNotFound} else if err != nil {return fmt.Errorf("запрос брони: %w", err)}switch status {case "cancelled":return ErrBookingCancelledcase "shipped":return ErrBookingShippedcase "active":// Выполняем отмену_, err = tx.ExecContext(ctx,"UPDATE bookings SET status = 'cancelled', updated_at = NOW() WHERE id = $1", bookingID)if err != nil {return fmt.Errorf("обновление статуса: %w", err)}// Возвращаем товар на склад_, err = tx.ExecContext(ctx,"UPDATE stock SET quantity = quantity + 1 WHERE booking_id = $1", bookingID)if err != nil {return fmt.Errorf("возврат на склад: %w", err)}default:return fmt.Errorf("неизвестный статус брони: %s", status)}return tx.Commit()}func isTemporaryError(err error) bool {// Проверка на временные ошибки БД (deadlock, timeout и т.д.)var pqErr *pq.Errorif errors.As(err, &pqErr) {return pqErr.Code == "40001" // serialization_failure}return errors.Is(err, context.DeadlineExceeded) || errors.Is(err, io.ErrUnexpectedEOF)}
Ключевые моменты:
-
Используйте уровень изоляции
Serializableдля предотвращения race conditions. -
Применяйте
SELECT ... FOR UPDATEдля блокировки строки. -
Разделяйте бизнес-ошибки (возвращаются клиенту) и технические (повторяются).
-
Логируйте все ошибки с контекстом (bookingID, попытка, ошибка).
-
Для распределённых систем добавьте идемпотентность (например, через уникальный ключ запроса).
> Похожие задачи по backend
Как гарантировать, что заказ не будет выполнен без исполнителя после резерва товара
Как снять бронь товара при отмене заказа
Как обнаруживать и реагировать на проблемы с отменой брони
Сколько разработчиков в команде, на каких языках они работают и как устроена команда
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью