Программирование GoОбработка ошибокGo-разработчик серверных приложений

Паника перехвачена через recover: содержит ли возвращённое значение трассировку стека?

Паника перехвачена через recover: содержит ли возвращённое значение трассировку стека?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Нет. recover возвращает только значение, переданное в panic, и сам по себе не добавляет к нему трассировку стека. Если стек нужен для диагностики, его следует отдельно получить во время выполнения deferred-обработчика, например через runtime/debug.Stack().

Исторический контекст

Механизм panic/recover в Go предназначен прежде всего для аварийного управления потоком выполнения: паника раскручивает стек, а recover позволяет границе приложения перехватить её. Значение паники отделено от диагностической информации, поэтому разработчик сам решает, как фиксировать стек, логировать событие и преобразовывать его в ошибку.

Такой подход не заставляет каждый перехватываемый случай автоматически содержать тяжёлую диагностическую информацию. При этом инфраструктурный код может добавить стек именно там, где это необходимо: например, на границе HTTP-сервера или worker-пула.

Постановка проблемы

Если middleware преобразует результат recover только в error, внешний код получит причину паники, но может потерять место возникновения сбоя. Текст значения паники обычно сообщает лишь симптом и не заменяет стек вызовов.

Отсутствие стека усложняет поиск ошибки, особенно если одна и та же паника возникает в разных путях выполнения. Обратная крайность — бездумно возвращать стек клиенту: он может раскрыть имена внутренних пакетов, структуру сервиса и чувствительные данные из аргументов или сообщений.

Подробное решение

recover возвращает исходное значение паники как any. Если паника была вызвана строкой, вернётся строка; если ошибкой — это значение ошибки; если другим объектом — соответствующий объект. Трассировка стека не является частью этого возвращаемого значения.

Получать стек следует внутри deferred-функции, которая непосредственно вызывает recover, пока обработчик ещё выполняется в контексте раскрутки паники:

package main import ( "fmt" "runtime/debug" ) func boundary() (err error) { defer func() { if v := recover(); v != nil { fmt.Printf("panic: %v %s", v, debug.Stack()) err = fmt.Errorf("internal failure") } }() panic("broken invariant") }

В примере recover получает только значение "broken invariant", а debug.Stack() отдельно формирует текст текущего стека. Для production-кода стек обычно записывают во внутренний лог или систему мониторинга, а клиенту возвращают стабильную внешнюю ошибку без внутренних деталей.

Важно не путать стек паники со стеком, который мог быть заранее включён в пользовательский тип ошибки. Если само значение паники содержит стек, например благодаря библиотечному типу, он является частью этого значения; стандартный recover его автоматически не создаёт.

Практический компромисс — перехватывать паники только на заранее определённых границах приложения. Внутренние функции обычно не должны превращать каждую панику в обычную ошибку: это может скрыть нарушение инварианта. Граница должна сохранить диагностические данные, корректно завершить запрос или worker и не раскрыть стек внешнему потребителю.

Ситуация из практики

HTTP-сервис перехватывает панику в middleware. Первый вариант возвращает клиенту fmt.Errorf("%v", recover()). Он прост, но теряет стек, а иногда ещё и раскрывает внутреннее сообщение паники.

Второй вариант отправляет клиенту полный результат debug.Stack(). Диагностика становится удобной, но наружу утекают внутренние пути выполнения и детали реализации; это неприемлемо для production API.

Выбранный вариант — вызвать recover, записать значение паники и стек во внутренний структурированный лог, затем вернуть клиенту обобщённую ошибку с идентификатором события. Такой дизайн сохраняет расследуемость сбоя и одновременно отделяет внутреннюю диагностику от внешнего контракта.

Что кандидаты часто упускают

1. Вопрос: Можно ли получить тот же стек после выхода из deferred-обработчика?

Нет, обычный recover уже не предоставит контекст завершённой паники после выхода обработчика. Если стек нужен, его нужно получить и сохранить внутри deferred-функции, где выполняется восстановление.

2. Вопрос: Добавит ли fmt.Errorf со значением паники трассировку стека автоматически?

Нет. Форматирование изменит представление значения и может сохранить его как причину при использовании %w, но стек от этого не появится. Трассировку нужно получить отдельно и передать в систему логирования либо явно включить в собственный диагностический тип.

3. Вопрос: Следует ли всегда преобразовывать перехваченную панику в обычную ошибку?

Нет. Это зависит от границы ответственности. На границе запроса или worker-пула преобразование часто оправдано, чтобы изолировать одну операцию и корректно сообщить о сбое. Внутри библиотеки без чёткого контракта такое преобразование может скрыть программную ошибку, поэтому иногда правильнее залогировать контекст и повторно вызвать panic.