При панике в глубоко вложенной функции, при каком условии recover действительно остановит раскрутку стека?
recover перехватывает панику только при вызове из функции, зарегистрированной через defer, причём вызов должен быть выполнен непосредственно этой отложенной функцией. Если поместить recover в обычный вызов из deferred-функции, паника не будет остановлена.
После успешного recover раскрутка стека прекращается, а выполнение возвращается за пределы функции, в которой был установлен defer. Значение, переданное в panic, доступно как результат recover.
В Go обычные ошибки принято возвращать явно через значение типа error. Такой подход делает ожидаемые сбои частью обычного потока управления и позволяет вызывающему коду решить, что делать дальше.
panic и recover предназначены для другого класса ситуаций: нарушения инвариантов, невозможного состояния или аварийного завершения текущего потока выполнения. Механизм defer был выбран как точка гарантированного выполнения очистки и обработки паники во время раскрутки стека.
Паника запускает раскрутку стека текущей горутины. При этом выполняются отложенные функции всех раскручиваемых вызовов; если ни одна из них корректно не вызовет recover, программа завершится аварийно.
Главный риск — ошибочно считать, что любой вызов recover внутри deferred-кода перехватит панику. Если вызов скрыт за дополнительной функцией, recover возвращает nil, и раскрутка продолжается. Это может привести к неожиданному завершению процесса или к потере диагностического контекста.
Надёжная форма — зарегистрировать анонимную или именованную функцию через defer и вызвать recover непосредственно внутри неё:
Во время panic Go начинает выполнять deferred-функции при раскрутке стека. Вызов recover распознаётся как перехват активной паники только в непосредственном теле функции, вызванной механизмом defer; после этого паника считается обработанной.
recover вне активной паники возвращает nil. Поэтому проверка через value != nil может быть неоднозначной, если в panic передали именно nil: такой вызов всё равно запускает панику, но отличить его результат от отсутствия активной паники по одному значению нельзя. На практике обычно передают ошибку или другое ненулевое значение.
Перехватывать следует только на осмысленной границе: например, в middleware HTTP-сервера или в контролируемом запуске фоновой задачи. Библиотекам обычно не стоит безусловно поглощать панику, потому что это скрывает дефекты и нарушает ожидания вызывающего кода.
recover действует только в текущей горутине. Паника в другой горутине не может быть перехвачена deferred-функцией вызывающей горутины. Кроме того, после recover выполнение не продолжается с места возникновения паники: функция, в которой произошла паника, не возобновляется.
В HTTP-сервисе сторонний обработчик изредка вызывает panic из-за ошибки валидации внутреннего состояния. Рассматривались три варианта:
recover в каждый обработчик. Это локализует сбой, но приводит к дублированию, разным форматам логов и риску случайно скрыть критическую ошибку.Выбран третий вариант: middleware непосредственно вызывает recover в своей deferred-функции, записывает стек и возвращает контролируемый ответ. Для фоновых горутин применён отдельный wrapper с таким же барьером, поскольку HTTP-middleware не охватывает другие горутины. В результате сбой одного запроса перестал завершать процесс, а причина осталась доступна в диагностике.
Дополнительный вопрос: что произойдёт, если deferred-функция сначала вызовет вспомогательную функцию, а уже та — recover?
Ответ: паника не будет перехвачена. Для recover важен не только факт нахождения внутри цепочки deferred-вызовов, но и непосредственный контекст: его должен вызвать сам deferred-функциональный вызов. Вспомогательную функцию можно использовать для подготовки логирования, но сам recover должен находиться непосредственно в функции, переданной в defer.
Дополнительный вопрос: продолжится ли выполнение функции после строки, вызвавшей panic, если recover сработал?
Ответ: нет. recover прекращает раскрутку паники, но не возвращает выполнение к месту возникновения panic. Паниковавшая функция завершается, а управление возвращается вызывающему коду после завершения deferred-обработки. Поэтому recover нельзя использовать как обычный механизм перехода назад в середину алгоритма.
Дополнительный вопрос: почему recover в HTTP-обработчике не защищает от паники в горутине, запущенной этим обработчиком?
Ответ: паника и раскрутка стека привязаны к конкретной горутине. Deferred-функции HTTP-обработчика выполняются в стеке обработчика, тогда как новая горутина имеет собственный стек и собственный набор defer. Для безопасного запуска фоновой работы нужен отдельный wrapper внутри этой горутины, который сам установит defer и непосредственно вызовет recover; иначе паника может завершить процесс.