В сценарии с panic передали указатель на тип ошибки, равный nil: почему проверка результата recover на nil может дать неверный вывод?
Проверка recover() == nil даст false, если в panic передан типизированный nil, например нулевой указатель на конкретный тип. Интерфейс, содержащий такой указатель, сам не равен nil: он хранит динамический тип и значение nil.
В Go обычные ошибки предназначены для ожидаемых отказов и явно передаются через возвращаемое значение. Механизм panic/recover нужен для аварийного прерывания текущего потока выполнения и восстановления на специально подготовленной границе, например в middleware или верхнеуровневом обработчике.
Такое разделение позволяет не смешивать штатное ветвление программы с обработкой нарушений инвариантов и действительно исключительных ситуаций. При этом значение паники передаётся через интерфейс, поэтому для него действуют правила представления типизированных nil в интерфейсах.
Рассмотрим обработчик, который считает отсутствие паники по условию recover() == nil. Если паника была вызвана типизированным nil-указателем, обработчик увидит ненулевой интерфейс и решит, что паника произошла.
Обратная ошибка также опасна: нельзя безоговорочно считать любое значение, полученное из recover, обычной ошибкой. Паника может содержать произвольное значение, включая строку, число, пользовательский тип или типизированный nil.
Интерфейсное значение состоит из динамического типа и динамического значения. Неинициализированный интерфейс nil не содержит ни типа, ни значения. Но интерфейс, в который помещён nil-указатель конкретного типа, содержит тип, поэтому он ненулевой.
Здесь recover возвращает интерфейс с динамическим типом *failure и нулевым указателем внутри. Поэтому сравнение самого интерфейса с nil ложно, хотя результат типового утверждения имеет значение nil.
Нельзя исправить проблему универсальным приведением любого результата recover к error: значение паники может не реализовывать этот интерфейс. Надёжный обработчик должен заранее определить контракт допустимых значений паники, отдельно распознавать ожидаемые типы и безопасно обрабатывать остальные значения.
Это отличается от panic(nil). В современных версиях Go по умолчанию такая паника представляется специальным ненулевым значением runtime.PanicNilError; поведение старых версий и режимов совместимости может отличаться. Типизированный nil-указатель при этом остаётся ненулевым интерфейсным значением независимо от этой особенности.
В HTTP-middleware команда решила преобразовывать результат recover в error, чтобы единообразно логировать аварии. В одном обработчике паника была вызвана значением типа *requestFailure, равным nil. Проверка recover() == nil не сработала, а последующее безусловное обращение к значению как к обычной ошибке привело к неверной диагностике и потенциальной повторной панике.
Рассматривались два варианта. Первый — считать recover() == nil достаточной проверкой; он прост, но не различает настоящий nil и типизированный nil. Второй — зафиксировать допустимый формат паник и проверять тип через безопасное утверждение, а неизвестные значения логировать отдельно; этот вариант требует немного больше кода, зато не делает необоснованных предположений о типе.
Выбран второй вариант: штатные ошибки возвращаются как error, а паники содержат только документированные значения. Результат — корректное различение обычной ошибки, типизированного nil и неожиданных значений, без потери исходной причины.
Вопрос: Может ли типовое утверждение r.(*failure) вернуть nil после успешного recover?
Ответ: Да. Успешное типовое утверждение проверяет динамический тип интерфейса, а не ненулевость содержащегося указателя. Если r содержит тип *failure и значение nil, утверждение вернёт нулевой указатель и true во втором результате.
Вопрос: Безопасно ли вызывать метод ошибки у результата recover, если его динамический тип — указатель со значением nil?
Ответ: Не обязательно. Само наличие метода в типе не гарантирует, что вызов метода корректен для нулевого указателя. Метод может безопасно обрабатывать nil-receiver, но если он разыменует receiver, произойдёт новая паника.
Вопрос: Почему нельзя использовать только проверку типа recover как признак того, что паника содержит ошибку?
Ответ: Потому что panic принимает произвольное значение, а не только error. Результат может быть строкой, числом, структурой или типизированным nil; поэтому обработчик должен либо поддерживать явно заданный набор типов, либо иметь безопасную ветку для всех остальных значений.