Может ли recover в одной горутине перехватить panic, возникшую в другой?

Может ли recover в одной горутине перехватить panic, возникшую в другой?

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

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

Нет. recover действует только внутри той же горутины, в которой возникла panic, и вызывается из отложенной функции во время раскрутки её стека. Для безопасного перехвата паники в рабочей горутине нужно установить defer с recover непосредственно внутри этой горутины; обработчик в вызывающей горутине её панику не увидит.

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

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

Обычные ожидаемые сбои в Go принято передавать как значения error. panic предназначена для аварийных ситуаций или нарушений внутренних предположений, а recover позволяет локализовать такую аварию на границе компонента, если это действительно необходимо.

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

Представим функцию, которая запускает рабочую горутину и сама ожидает её завершения. Если паника возникнет в рабочей горутине, defer с recover во внешней функции не сработает: внешняя функция находится на другом стеке.

Неверная попытка перехватить панику снаружи может привести к завершению всей программы вместо передачи сбоя как ошибки. Особенно опасно это для серверов, очередей и фоновых обработчиков, где одна неисправная задача не должна автоматически останавливать весь процесс.

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

Каждая горутина должна сама установить границу восстановления, если компонент допускает преобразование паники в ошибку. recover возвращает значение паники только при вызове из отложенной функции той же горутины и в момент, когда эта горутина раскручивает стек из-за паники.

Минимальный пример:

package main import "fmt" func runSafely() (err error) { defer func() { if value := recover(); value != nil { err = fmt.Errorf("worker panic: %v", value) } }() panic("broken invariant") } func main() { results := make(chan error, 1) go func() { results <- runSafely() }() fmt.Println(<-results) }

Здесь recover выполняется в той же горутине, что и panic, поэтому паника превращается в ошибку и передаётся через канал. Если убрать runSafely и разместить recover только в main, он не перехватит панику рабочей горутины.

Перехватывать следует не каждую панику без разбора. Нужно решить, можно ли после неё безопасно продолжать работу: паника может указывать на повреждение инвариантов или неконсистентное состояние. Также важно передавать результат выполнения через канал, errgroup или другой согласованный механизм, а не пытаться использовать recover как замену обычной обработке error.

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

В HTTP-сервисе каждый запрос обрабатывается в отдельной горутине. В одном обработчике обнаружилась ошибка программиста, вызывающая panic. Если серверный слой не устанавливает восстановление в горутине запроса, авария может завершить весь процесс.

Рассматривались два варианта. Первый — поставить recover только вокруг вызова запуска обработчика: это не работает, потому что обработчик выполняется в другой горутине. Второй — добавить defer с recover в middleware, выполняемом внутри горутины запроса: это изолирует сбой, позволяет записать стек и вернуть клиенту контролируемый ответ.

Выбран второй вариант, но только как защитная граница сервера. Внутри бизнес-логики ожидаемые ситуации по-прежнему возвращаются через error, а после восстановления запрос помечается неуспешным и состояние, потенциально затронутое паникой, не переиспользуется. В результате единичная ошибка не обрушивает процесс, но скрытые дефекты не маскируются обычной логикой обработки ошибок.

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

  1. Достаточно ли вызвать recover в любой функции той же горутины?

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

  1. Можно ли передать значение паники в другую горутину и вызвать там recover?

Передать само значение можно, но это уже не восстановление исходной паники. recover работает только с активной паникой текущей горутины; полученное через канал значение можно лишь обработать как обычные данные или ошибку. Чтобы преобразовать исходную панику, восстановление нужно выполнить до отправки результата в другую горутину.

  1. Что произойдёт, если в горутине случится паника и там не будет подходящего recover?

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