MBL
Go / avito / Тестовое задание / Каталог реальных багов, найденных на ревью собственного кода
Go средний

Каталог реальных багов, найденных на ревью собственного кода

avitoreviewpostgresdocker

По словам интервьюера, кандидат, не прошедший этот собес, потерял бы 10% пользователей на непокрытом крае — не из-за незнания языка, а потому что сам не нашёл этот край заранее. Прямая тренировка обратного навыка — уметь предъявить конкретный список того, что сам у себя нашёл и починил, а не общее «я тестировал, всё работает». Ниже — реальные находки на «Avito.Кухне», не гипотетические примеры.

Логические баги

Дубль item_id в одном заказе. Список ID для похода в БД строился с дублями, количество — в map без дублей; репозиторий схлопывал дубли сам через WHERE id = ANY($1), и код получал меньше позиций, чем ожидал → misleading 404 вместо суммирования количества. Разобрано подробно в «Конкурентные остатки». Урок: две коллекции по одному и тому же ключу должны строиться из одного источника.

Невалидный fulfillment_type маппился в 409, а не в 400. CreateOrder при провале валидации FulfillmentType.IsValid() возвращал существующую, но не ту ошибку — формально запрос отклонялся, но с кодом, никак не описывающим реальную причину отказа (переход статуса тут вообще ни при чём). Найдено на отдельном ревью, не при написании кода — потому что тест на happy path и тест на «сток не хватило» не покрывали именно этот конкретный вход:

До
if !input.FulfillmentType.IsValid() {    return domain.Order{}, domain.ErrInvalidTransition // -> 409 invalid_transition}
После
if !input.FulfillmentType.IsValid() {    return domain.Order{}, domain.ErrInvalidFulfillmentType // -> 400 invalid_fulfillment_type}

Отрицательные price_cents/stock падали на DB CHECK, а не в коде. MenuUsecase.Create/Update не проверяли неотрицательность перед отправкой в БД — ограничение отлавливалось Postgres CHECK-constraint'ом, а ошибка constraint'а всплывала клиенту как generic 500 internal_error вместо 400. Технически «работало» (некорректный ввод отклонялся), но неправильным способом — клиент не мог отличить свою ошибку валидации от реального сбоя сервера:

До
func (uc *MenuUsecase) Create(ctx context.Context, in CreateItemInput) (domain.Item, error) {    return uc.items.Create(ctx, in.ToItem()) // отрицательная цена/сток - падает на CHECK, 500}
После
func (uc *MenuUsecase) Create(ctx context.Context, in CreateItemInput) (domain.Item, error) {    if in.PriceCents < 0 {        return domain.Item{}, domain.ErrInvalidPrice // -> 400    }    if in.Stock < 0 {        return domain.Item{}, domain.ErrInvalidStock // -> 400    }    return uc.items.Create(ctx, in.ToItem())}

Списочные ручки заказов отдавали items: null. GetByID/GetForUpdate в OrderRepository всегда дозагружали order_items, а ListByUser/ListByEstablishment — нет: возвращали заказы вообще без позиций. Незаметно там, где список используется только для статусов — фатально там, где список — единственный источник данных о содержимом заказа. Починка — общая батч-подгрузка WHERE order_id = ANY(...) для всех списочных методов, не N+1 по одному заказу.

payload_json заполнялся в двух несовместимых формах. Пока в проекте существовали два пути получения заказа establishment-service'ом (push и упразднённая позже поллинг-реконсиляция), один путь сохранял одну JSON-схему, другой — другую. Клиент, читающий payload_json, не мог полагаться на фиксированную форму. Урок общий, не только про этот проект: если одни и те же данные попадают в систему двумя разными путями, они обязаны попадать в одной и той же форме — либо унифицировать путь, либо унифицировать сериализацию до записи.

Инфраструктурные баги, пойманные только реальным прогоном

Гонка на первом рестарте Postgres. После docker-entrypoint-initdb.d-скрипта, создающего вторую БД, Postgres делает полный внутренний рестарт. Healthcheck (pg_isready) на первом прогоне с пустым volume успевал зафиксировать «healthy» до этого рестарта — зависимые сервисы (migrate-kitchen/migrate-establishment) стартовали по этому сигналу и ловили connection refused, хотя формально Postgres уже был healthy. Невозможно найти чтением кода — только реальным docker compose up -d с чистого volume. Починка — restart: on-failure:5 на миграционных сервисах; depends_on: condition: service_completed_successfully у остальных продолжает ждать именно успешного завершения, гонка становится прозрачной (на пару секунд дольше первый старт, не ошибка).

Несуществующий тег Docker-образа. go.mod фиксирует go 1.26.5 — точную версию локального тулчейна, но в Docker Hub нет тега golang:1.26.5-alpine, только golang:1.26-alpine (на деле содержит более новый патч). Не баг рассинхрона: Go разрешает собирать модуль тулчейном новее указанного в go X.Y.Z, но не старее без подтягивания версии из сети — раз образ содержит версию не старше объявленной в go.mod, скрытого похода в сеть при сборке не происходит.

Как эти находки формулировать на собесе

Не «у меня всё сразу работало» (это будет звучать неубедительно и провоцирует уточняющие вопросы, на которые не будет готового ответа) и не список оправданий. Рабочая формула на каждый пункт: что сломалось → как обнаружено (код-ревью / реальный прогон / тест) → почему это конкретное решение чинит именно эту причину, а не симптом.

У находок выше есть общее свойство, которое стоит подчеркнуть явно: ни одна не найдена «повезло, заметил случайно» — каждая либо поймана отдельным явным ревью после первой реализации, либо воспроизведена реальным прогоном инфраструктуры (не выведена умозрительно). Это и есть содержательный ответ на «а как ты вообще проверяешь свой код перед сдачей».

Самопроверка 0 / 3
Умею назвать конкретный найденный у себя баг, а не общие слова "тестировал"
Понимаю паттерн "две коллекции из одного источника" как источник расхождений
Знаю разницу между "должно работать по коду" и "прогнано вживую"
Как усвоено?