🧑💻 Четыре года разработки без документацииКод собирался. Тесты проходили. Но никто в команде не мог объяснить, зачем существуют три пакета. Инженер получил сервис на 47 000 строк Go. Зелёный CI, четыре года в проде, последние 14 месяцев без серьёзных инцидентов.
По всем видимым признакам здоровый проект. Но через три недели у него был список вещей, которые никто не мог объяснить. Не из-за слабой команды, а потому что люди, знавшие ответы, ушли, и ответы ушли вместе с ними.
Долг был не в качестве кода, а в пониманииЗдесь ломается привычное определение. Обычно техдолг это плохой код. Спагетти, нет тестов, устаревшие зависимости. Но в этом проекте покрытие было 71%, стиль ровный, зависимости под контролем, сборка чистая. Метрики говорили, что всё в порядке.
Кодовая база это запись решений. Каждая граница пакета, каждый интерфейс, каждый слой абстракции когда-то были решением, принятым в конкретном контексте. Четыре года спустя решения остались, а причины исчезли.
Работать с кодом, чью логику структуры вы не можете восстановить, это не то же самое, что работать с плохим кодом. Вы не отличите «так сделано по хорошей причине, которую я пока не понял» от «так сделано из-за ограничения, которого давно нет». Каждое изменение несёт эту неопределённость.
➡️
Три пакета, которые никто не смог объяснитьsync2 повторял куски
x/sync из стандартной библиотеки. Семафоры, errgroup, пул. Из 11 мест импорта семь тянули только
Pool, у которого в стандартной библиотеке есть почти идентичный
sync.Pool. Скорее всего, когда сервис писали, стандартных примитивов не хватало,
sync2 был затычкой. Потом стандартная библиотека догнала, а затычка осталась.
cache это локальный кэш в памяти с TTL. При этом в сервисе был Redis. Часть данных шла в Redis, часть в локальный кэш, часть в оба, и нигде не было записано, что куда должно идти. По коммитам картина восстанавливается. Redis добавили на втором году из-за проблемы с задержками, а миграция осталась частичной.
transport определял кастомный HTTP-транспорт с ретраями и настройкой пула. Исходящих вызовов было три, два использовали
transport, один обычный
http.DefaultClient. Никакой причины для разницы. Это было не решение, а накопление. Разные инженеры добавляли вызовы, кто-то не знал про
transport, кто-то не посчитал, что он подходит.
➡️
Зелёные тесты врутТесты проверяют поведение. Они не проверяют, что поведение вообще нужно, что абстракция под ним правильная, что это тот код, который должен решать задачу. У
transport было 23 теста, все проходили. Они проверяли, что ретраи работают и пул настраивается. Они не проверяли, нужен ли этот транспорт вообще.
Вот показательный кусок. У этой функции было четыре теста, все зелёные, но никто не знал, что значит «legacy» в контексте:
func (c *Client) fetchWithLegacyFallback(ctx context.Context, req Request) (*Response, error) {
resp, err := c.primary.Fetch(ctx, req)
if err == nil {
return resp, nil
}
// If primary fails, try legacy endpoint
// TODO: remove when migration complete
return c.legacy.Fetch(ctx, req)
}Комментарию TODO было два года. Миграция, на которую он ссылался, нигде не задокументирована.
legacy-клиент всё ещё вызывался при каждой ошибке. Никто не знал, завершилась миграция или идёт до сих пор.
Налог на онбордингВремя на эту археологию не бесплатное. По заметкам автора счёт вышел такой:
• 11 часов на три загадочных пакета
• 9 часов на разбор кэша против Redis
• 7 часов на цепочку middleware, которая протаскивала значение сквозь четыре слоя ради трейсинга. Библиотеку трейсинга заменили годами раньше, а значение осталось
• 4 часа на разбор
utils/Итого около 31 часа, три четверти рабочей недели. И этот счёт оплатит заново каждый следующий инженер, который придёт в проект.
➡️
Оригинал📍 Навигация:
Вакансии •
Задачи •
Собесы🐸
Библиотека Go-разработчика#GoDeep