Команда Go for Devs подготовила перевод статьи о том, как отлаживать приложения на Go. Автор показывает, что баги бывают разными: воспроизводимые, случайные, гейзенбаги и конкурентные. А в арсенале Go-разработчика должны быть — TDD, стратегическое логирование, Delve, git bisect и даже онлайн-отладчик GoTutor.
Не все баги одинаковы. Одни легко обнаружить и устранить с помощью отладчика. Другие могут проявляться редко или быть крайне неочевидными — искать их всё равно что искать иголку в стоге сена. Выбор правильной техники отладки способен существенно повысить шансы на успех и сократить время на решение.
Все знают: отладка вдвое сложнее, чем написание программы. Так что если вы уже вложили максимум ума в написание кода, как же вы потом собираетесь его отлаживать?
— Брайан У. Керниган, The Elements of Programming Style
Признаюсь: я использую отладчик от силы раз в сто лет. Мне ближе подход «раскидать по коду пару Printf
и посмотреть, что выйдет». (Я даже сделал небольшой пакет для отладочного логирования с выводом прогресса и состояния, который полностью исчезает из продакшен-сборки, не оставляя ни малейшего шанса случайно утечь конфиденциальным данным.)
Однако вопрос «использовать ли отладчик» — это не про стиль и не про предпочтения. В одном проекте вам может хватить тестов и пары логирующих строк, а в другом вы будете чувствовать себя потерянным без ежедневной дозы отладки.
Поэтому, если хотите действительно преуспеть в отладке, вам нужен целый арсенал стратегий и приёмов (включая «предотладочные»), и начинать стоит с хорошей подготовки.
Предварительные шаги
Самый простой баг — тот, которого удалось избежать. Второй по простоте — тот, что удалось поймать на раннем этапе. Несколько мер, предпринятых заранее, могут сыграть решающую роль.
Пишите понятный, читаемый и удобный для отладки Go-код
Отладка начинается задолго до запуска отладчика.
Как только вы начинаете продумывать разрабатываемую систему, ваши мысли и решения уже влияют на то, насколько удобно её будет отлаживать. Поэтому сначала максимально чётко сформулируйте задачу, которую решаете. А затем используйте эту ясность, чтобы писать ясный код.
Накопленный опыт породил множество парадигм, методологий, принципов и техник, которые помогают создавать надёжное ПО с минимумом багов и удобной отладкой, в том числе:
Все подходы, которые поощряют слабую связанность. Сюда относятся любые многоуровневые архитектуры, например Clean Architecture или Hexagonal Architecture.
Метапринцип SOLID, включающий принципы единой ответственности, открытости/закрытости, подстановки Барбары Лисков, разделения интерфейсов и инверсии зависимостей.
Я не буду углубляться в детали — этим темам посвящены целые книжные полки, и их стоит изучать отдельно.
Как писать ясный код?
Начинайте с простого и следуйте идиоматике Go:
Делайте функции небольшими и сосредоточенными на одной задаче
Выбирайте короткие, но выразительные имена
Пишите максимально самодокументирующийся код («понятность важнее хитроумности»)
Добавляйте комментарии на уровне пакетов и функций
Предпочитайте ранний выход из функции вместо вложенных конструкций
if-else
Избегайте глобальных переменных
Используйте систему интерфейсов Go для гибкого и модульного дизайна
Будьте последовательны в стиле кода, но при этом придерживайтесь правил, принятых в команде
Практик гораздо больше — оставлю это вам в качестве самостоятельного упражнения: изучать их и внедрять в работу.
Профессиональный совет: следуйте «правилу бойскаута» — оставляйте код чище, чем он был до того, как вы его нашли.
Используйте разработку через тестирование (TDD) как стратегию отладки
Пишите тесты, чтобы зафиксировать ожидаемое поведение, ещё до того как реализуете это поведение в коде.
Две причины:
Тесты подтверждают, что вы правильно поняли задачу.
Тесты помогают рано ловить баги и предотвращать регрессии.
Спецификации, написанные простым английским, важны, но они не связаны напрямую с кодом. Модульные, интеграционные и сквозные (end-to-end) тесты — это мост между спецификацией и кодом. Хорошо написанный тест — почти как формальная спецификация кода, только без необходимости использовать язык формальных спецификаций. Вы можете оставаться в знакомом языке программирования и описывать, что именно должен делать код.
Помните: тесты запускаются для вас автоматически; отладку приходится делать вручную.
Постарайтесь покрыть тестами как можно больше граничных случаев — именно они часто становятся источником багов, которые всплывают недели через три после вывода кода в продакшен. Используйте все виды тестов, которые предлагает ваш язык: в Go из коробки есть модульные тесты, включая табличные тесты, fuzz-тестирование, golden-файлы и многое другое. Игнорируйте эти полезные стратегии на свой страх и риск.
Когда все тесты на месте, у вас появляется больше свободы менять код. Тесты служат постоянной страховкой: вы можете вносить изменения, зная, что любое нарушение ожидаемого поведения сразу будет подсвечено. Помните: тесты запускаются для вас автоматически; отладку приходится делать вручную.
Стратегическое логирование: загляните внутрь «чёрного ящика» вашего кода
Вывод логов бесценен для отладки. Если логирующие инструкции расставлены правильно, они создают хронологию событий, которая привела к конкретному результату — включая баг, который вы пытаетесь найти.
Общее правило: добавляйте логирование везде, кроме публичных пакетов. Вы не должны навязывать своим пользователям определённый стиль логирования или конкретную библиотеку. Логирование — это задача приложения и его внутренних пакетов.
Однако правильно расставить точки логирования непросто: нужно найти баланс между количеством информации и накладными расходами. Жёстких правил здесь нет — выбор всегда за вами. Ситуацию усложняет ещё и то, что существуют разные «школы» логирования. Более того, логи полезны не только при отладке. Они помогают выявлять проблемы, не связанные напрямую с багами в коде, например, попытки несанкционированного входа в систему. Всё это увеличивает количество решений, которые приходится принимать при выборе мест для логирования.
В любом случае минимум логирования должен охватывать:
Любые ошибки и сбои валидации, возникающие в вашем коде или сообщаемые библиотеками;
События, связанные с безопасностью, например аутентификацию, авторизацию, а также потоки данных в приложение и из него.
Каждая запись в логах должна содержать достаточно контекста, чтобы быть полезной. Начните с «пяти W»:
Когда произошло событие? (when – метка времени)
Что произошло? (what – тип, уровень серьёзности, описание)
Где это произошло? (where – ID приложения, имя сервиса, место в коде)
Кто это сделал? (who – пользователь — человек или система)
Почему это произошло? (why – особенно важно в случае ошибок)
История о «безконтекстной» низкоуровневой ошибке в Spotlight наглядно показывает, почему контекст имеет решающее значение.
Для дополнительной информации посмотрите OWASP Logging Cheat Sheet.
Совет для Go: используйте пакет slog
во всех сколько-нибудь серьёзных приложениях. Рано или поздно вы оцените структурированный вывод логов и возможность подключения разных бэкендов для автоматической обработки журналов.
Временные техники логирования для отладки
Логирование — это стратегическое решение, которое принимается на этапе написания кода. Но бывает, что при поиске бага вы обнаруживаете: участок кода, представляющий наибольший интерес для диагностики, либо даёт недостаточно информации в логах, либо вовсе ничего не пишет. Если это связано с отсутствием нужной логирующей инструкции — добавьте её и продолжайте отладку.
Однако иногда логирование требуется лишь для разбора конкретной ситуации. В таких случаях вывод в лог не несёт ценности в общем контексте.
Здесь на помощь приходит так называемое «ad-hoc логирование». Такой подход позволяет отслеживать работу кода на сколь угодно детальном уровне, не вмешиваясь в остальное логирование, потому что после нахождения и исправления бага эти временные инструкции можно просто отключить или удалить. Схема проста:
расставьте временные логирующие инструкции в нужных местах, чтобы собрать больше информации;
перекомпилируйте приложение и воспроизведите ошибку;
когда задача решена — удалите или отключите временные инструкции.
Эта техника особенно удобна в таких языках, как Go, где компиляция занимает считаные секунды.
Самый эффективный инструмент для отладки — это вдумчивый анализ, дополненный грамотно расставленными выводами на печать.
— Брайан У. Керниган
Иногда отладка требует логировать чувствительные данные. Чтобы гарантировать, что они никогда не попадут в продакшен-логи, я написал и использую пакет what, который управляет ad-hoc логированием через компиляционные теги. Благодаря этому никакие секретные данные случайно не утекают в боевую среду.
В завершение: добавление пары дополнительных логирующих инструкций в проблемных местах кода часто помогает локализовать баг.
Когда баги атакуют: отладка приложений на Go
Первое правило отладки: всегда ищите первопричину.
Как только вы обнаружили баг или получили уведомление о нём, не спешите с головой бросаться в сессии с отладчиком. Моё главное правило при поиске бага — всегда стремиться найти первопричину. Иначе вы лишь устраняете симптомы, а сам баг вскоре проявится снова.
Что стоит сделать в первую очередь:
изучите лог-файлы;
определите условия, при которых возникает баг;
постарайтесь получить воспроизводимый тест-кейс, лучше всего от того, кто сообщил о баге (ведь на этом этапе он знает о нём больше вас);
обсудите проблему с коллегами, напарниками по коду или любым участником проекта. Иногда вторая пара глаз — на вес золота.
Баги коварны. Одни лежат на поверхности и проявляются при чётко воспроизводимых условиях. Другие случаются время от времени, оставляя лишь следы (или логи) для анализа. Третьи проявляются только в специфической среде — чаще всего в продакшене, где использование «агрессивных» техник отладки попросту невозможно. Есть и такие, что загадочным образом исчезают, как только их начинают отслеживать, и снова возникают, когда за ними никто не наблюдает.
Подобно сорнякам, разные виды багов требуют разного подхода к лечению.
Воспроизводимые баги
Когда я работал в техподдержке, любое неожиданное поведение, о котором сообщал клиент, всегда разбиралось с одного и того же шага: мы просили его описать последовательность действий, при которых поведение можно воспроизвести.
У воспроизводимого поведения есть несколько преимуществ:
все участники (обычно клиент, техподдержка и инженеры) могут согласовать наблюдаемое поведение;
если каждый может воспроизвести баг у себя, проще обсуждать проблему;
поведение, которое можно воспроизвести по запросу, можно тщательно тестировать и изучать;
классические техники отладки (отладчик, временное логирование, «точечно расставленные принты» и т. п.) работают отлично;
исправление можно подтвердить сразу же (особенно если вы поставили «страховочные барьеры» в виде добавленных тестов).
С воспроизводимым багом отладка значительно проще:
Начните с имеющейся информации о баге и найдите участок кода, где происходит сбой.
Определите контекст максимально точно. Одна и та же функция может вызываться из разных мест. Постарайтесь определить точную цепочку вызовов через стек-трейсы и/или логи, чтобы собрать максимум данных о том, что привело к некорректному поведению.
Подключайте все доступные техники: изучайте код, стройте мысленную модель того, что может идти не так; затем добавляйте
log.Printf()
илиwhat.Happens()
, либо запускайте Delve, чтобы заглянуть туда, где нужна ясность.
На любом этапе не стесняйтесь обсуждать баг с другими. А если рядом никого нет — всегда можно обратиться к любимому AI-ассистенту для кода.
Случайные баги
Редко возникающие баги могут быть особенно коварными. Без детерминированного сценария воспроизведения найти проблемный участок кода почти невозможно. Поэтому главная цель — превратить «случайный» баг в воспроизводимый.
Вот несколько приёмов, которые помогут приблизиться к источнику проблемы:
попросите того, кто сообщил о баге, обратить внимание на возможные закономерности. Параллельно попытайтесь выявить повторяющиеся паттерны самостоятельно;
изучите все доступные логи и другую информацию об исполняемой системе;
ищите отклонения в логах от «нормального» поведения, с которым вы привыкли работать;
проверьте метрики: они могут указать на ненормальные состояния, например, нехватку памяти или необычное потребление ресурсов (файлы, базы данных и т. п.);
отправьте своих «шпионов» — в продакшене поднимите уровень логирования настолько, насколько это приемлемо для операторов. (Помните, что собирать логи, возможно, придётся длительное время — до следующего проявления бага.);
если удалось заметить ошибку в непроизводственной среде, добавьте больше временных логирующих инструкций;
если баг проявляется через конкретное сообщение об ошибке, двигайтесь назад от источника ошибки, выявляя условия, которые её вызывают, и проверяйте их, пока не добьётесь стабильного воспроизведения при определённых тестовых условиях.
На этом этапе случайный баг превращается во воспроизводимый, и можно продолжать работу с применением уже описанных методов для таких случаев.
Гейзенбаги
Особенно неприятная разновидность случайных багов — гейзенбаги. Название отсылает к Вернеру Гейзенбергу, немецкому теоретическому физику, сформулировавшему принцип неопределённости. Если объяснять простыми словами, этот принцип утверждает: невозможно одновременно точно измерить некоторые пары свойств элементарной частицы, например её положение и импульс, потому что сам процесс наблюдения искажает состояние частицы.
По аналогии с принципом неопределённости, гейзенбаги — это баги, которые словно исчезают или меняют своё поведение, когда кто-то пытается воспроизвести их намеренно.
Например, добавление логирующих инструкций в чувствительный к времени код может изменить тайминг так, что ошибка перестанет проявляться. Но стоит убрать лишние логи — и поведение возвращается.
Чтобы вычислить гейзенбаг, приходится отказываться от «инвазивных» техник отладки и переходить к пассивному наблюдению, внимательному изучению кода и серьёзным размышлениям. (На момент написания у меня нет данных о том, насколько хорошо LLM-модели справляются с гейзенбагами, если им предоставить код, описание симптомов и известных условий возникновения.)
Баги конкурентности
Конкурентность добавляет в поведение кода новое измерение. Когда выполнение может разделяться на несколько потоков, тайминг внезапно становится куда важнее, чем в однопоточном коде. Например, конкурентный код может одновременно записывать данные в одну и ту же сущность (это называется гонкой данных), или две горутины могут взаимно заблокироваться, ожидая освобождения ресурса друг от друга.
Отладка конкурентного кода может быть очень сложной. Здесь особенно много гейзенбагов: любое вмешательство в работу программы меняет тайминг (незаметно — если вы вставляете временные логирующие инструкции, и гораздо заметнее — если останавливаете горутину на брейкпоинте, пока другие продолжают выполняться).
Вот несколько техник, которые помогут вычислять баги конкурентности:
включайте детектор гонок данных, чтобы выявлять гонки в коде;
используйте pprof для генерации профилей по CPU, памяти, горутинам, аллокациям, блокировкам синхронизации или конфликтам за мьютексы;
применяйте go tool trace, чтобы анализировать трассировки, которые можно получить через
runtime/trace.Start()
, пакетnet/http/pprof
или передачей флага-trace
вgo test
;симулируйте однопоточную среду, вызвав
runtime.GOMAXPROCS(1)
или установив переменную окруженияGOMAXPROCS
в значение 1.
Delve — основной отладчик для Go
Я уже упоминал Delve — пожалуй, самый популярный отладчик для Go. Если в вашей Go-IDE есть встроенный отладчик, скорее всего, под капотом у него работает именно Delve.
С Delve вы можете пошагово проходить код приложения или тестов, просто вызвав:
dlv debug [<path-to-main-package>]
или
dlv test [<path-to-package-with-tests>]
Эти команды запускают REPL Delve. Оттуда можно ставить брейкпоинты и продолжать выполнение до первого из них, пошагово проходить строки кода или просматривать значения переменных.
Я не буду углубляться в подробности использования Delve — это легко может потянуть на ещё пару статей. За детальной информацией о настройке и применении Delve обратитесь к документации на pkg.go.dev.
Отладка через бинарный поиск в истории Git
Пошаговый проход кода в отладчике может быть утомительным, особенно если вы не представляете, где именно скрывается баг — ведь он может не выдавать явного сообщения об ошибке со стек-трейсом и номерами строк.
Знаете ли вы, что система контроля версий — конкретно Git — может помочь сузить область поиска?
Эта техника называется git bisect, и работает она так:
Любой баг появляется между двумя коммитами: в одном код ещё работал исправно, а в следующем — уже ломался. (Если баг был прямо в самом первом коммите, то и кода для анализа будет немного.) Поэтому, если последовательно проверять более ранние версии, пока вы не найдёте оба этих коммита, можно быстро выделить изменения, ответственные за баг.
Git для этого предоставляет команду bisect
. Всё, что вам нужно, — это «плохой» коммит (где баг точно есть) и «хороший» коммит (где бага точно нет).
Дальше процесс выглядит так:
bisect
выбирает коммит посередине между хорошим и плохим и делает checkout.Вы компилируете и тестируете код. В зависимости от результата помечаете коммит как «good» или «bad».
Теперь диапазон между «хорошим» и «плохим» сузился.
bisect
выбирает следующий коммит в этом диапазоне, и процесс повторяется, пока не будет найден первый «плохой» коммит.
После этого можно сравнить его с предыдущим и проанализировать конкретные изменения в коде.
Приятная особенность метода: неважно, насколько далеко друг от друга находятся выбранные «good» и «bad» коммиты. Каждый шаг делит диапазон пополам, и для нахождения первого плохого коммита в диапазоне из n коммитов понадобится всего log₂(n) шагов. Например, для 100 коммитов между исходными «good» и «bad» потребуется всего 7 итераций.
Подробнее о команде bisect
читайте в документации git-bisect.
Русскоязычное Go сообщество

Друзья! Эту статью перевела команда «Go for Devs» — сообщества, где мы делимся практическими кейсами, инструментами для разработчиков и свежими новостями из мира Go. Подписывайтесь, чтобы быть в курсе и ничего не упустить!
Бонус-совет: GoTutor — простой вход в отладку
Запуск отладчика, пошаговое выполнение кода и просмотр глубоких структур данных могут показаться пугающими для новичков. Познакомьтесь с GoTutor — онлайн-отладчиком Go, который позволяет пошагово проходить код и наглядно показывает, как появляются и исчезают переменные, горутины и стек-фреймы.
Вот скриншот примерной сессии отладки:

Заметили кнопки Prev и Next? Вы можете не только двигаться вперёд, но и возвращаться назад по коду, повторяя шаги столько раз, сколько нужно. Это простой, но очень эффективный способ глубже понять, как устроен поток выполнения.
Разве это не отличный и увлекательный способ учиться отладке?
FAQ: Развенчиваем мифы об отладке и не только
Какие ошибки чаще всего допускают при отладке?
Вот пять распространённых ошибок разработчиков:
Слишком сильное погружение в детали. Не зацикливайтесь на строках кода, не учитывая общей картины.
Игнорирование инструментов. В зависимости от ситуации может хватить пары логирующих инструкций, а может понадобиться весь арсенал возможностей отладчика.
Спешка. Давление «поскорее починить» часто ведёт к поспешным решениям и упущенным деталям. В итоге вы либо лечите только симптомы вместо причины, либо тратите кучу времени на бесполезную отладку.
Отсутствие тестирования после правок. Убедитесь, что ваши исправления не породили новые баги и не сломали старую функциональность.
Неправильное использование системы контроля версий. Коммитьте достаточно часто, чтобы иметь возможность откатиться и эффективно применять
git bisect
.
Отладчики нужны только для отладки, верно?
Не совсем. Отладчики действительно созданы для отладки, но их можно использовать и иначе. Например, если вам достался сложный или плохо написанный код, отладчик поможет «раскопать» его и понять, что делает конкретная функция или как используется структура данных.
Можно ли автоматизировать отладку?
Сама по себе отладка — процесс ручной (см. раздел про TDD), но некоторые аспекты можно автоматизировать.
Ваш код может быть отличным помощником в этом: пакеты runtime
и runtime/debug
позволяют исследовать внутренности приложения во время работы. Среди возможных кейсов:
дамп стека горутины;
принудительный запуск сборщика мусора (с возвратом памяти ОС);
просмотр счётчиков (количество работающих горутин, доступных CPU, вызовов CGO и др.);
получение статистики по памяти, профилей памяти и мьютексов;
изменение ограничений (максимальное число потоков ОС, «мягкий» лимит памяти, максимальный размер стека горутины).
С помощью флага или feature flag можно включать и выключать такие команды без перекомпиляции или перезапуска приложения. Отличный способ частично автоматизировать отладку.
Какие отладчики можно использовать с Go?
В первую очередь — Delve, нативный для Go отладчик, хорошо интегрированный с IDE и редакторами. Но если вам нужно отлаживать на платформе, где Delve не поддерживается, можно воспользоваться GNU Debugger (GDB). Он менее удобен в связке с Go, но всё же остаётся рабочей альтернативой.
Почему дефекты ПО называют “bug”?
Термин «bug» для ошибок появился ещё в инженерном жаргоне 1870-х, задолго до изобретения современного софта.
Забавный факт: в 1947 году в электромеханическом компьютере нашли «железный баг» буквально — мотылька, застрявшего в реле. (Подробности по ссылке выше.)