Как обрабатывать ошибки при отмене брони на складе
Что проверяют
Это вопрос про проектирование, а не про синтаксис if err != nil. Интервьюер хочет увидеть, что вы различаете виды ошибок и понимаете: отмена брони это компенсирующая операция, которую будут повторять. Следом спрашивают, что будет, если отмена пришла дважды или упала по таймауту.
Как отвечать
Разделите ошибки на классы, от этого зависит реакция:
- бизнес-ошибки: бронь не найдена, уже отменена, товар отгружен. Объявляются как sentinel-значения
var ErrAlreadyShipped = errors.New(...)или свой тип, проверяются черезerrors.Isиerrors.As. Повторять их бессмысленно, наружу уходят как 404 или 409; - временные: таймаут базы, недоступный соседний сервис. Повтор с экспоненциальной задержкой и джиттером в пределах общего дедлайна из
context.WithTimeout; - неизвестный исход: запрос ушёл, ответа нет. Самый опасный случай, и решается он идемпотентностью.
Каркас реализации:
- идемпотентность: повторная отмена уже отменённой брони возвращает успех. Для внешних вызовов передаётся ключ идемпотентности;
- атомарный переход:
UPDATE reservations SET status = 'cancelled' WHERE id = $1 AND status = 'reserved'и возврат остатка в одной транзакции.RowsAffected() == 0значит, что статус уже другой: перечитайте бронь и выберите правильную ошибку; - события: другие сервисы узнают об отмене через transactional outbox, чтобы запись в базу и событие не разошлись;
- контекст в ошибке:
fmt.Errorf("cancel reservation %s: %w", id, err), логирование один раз на границе обработчика, а не на каждом уровне.
Где спотыкаются
Говорят про panic и recover как аналог try-catch. В Go так обходятся только с невосстановимыми ситуациями, бизнес-ошибка брони к ним не относится.
Сравнивают ошибки по тексту или через == после обёртки. После %w надёжно работает только errors.Is, а текст сообщения может поменяться в любой момент.
Делают отмену неидемпотентной: повтор после таймаута возвращает остаток на склад второй раз, и учёт уезжает в плюс.