В сервисе на Go 1.20 обработчик считает nil от recover признаком отсутствия паники: что произойдёт после panic(nil)?
В Go 1.20 и более ранних версиях recover после panic(nil) возвращает nil, но саму панику останавливает. Поэтому обработчик может ошибочно решить, что паники не было, хотя выполнение функции уже прервано и управление перейдёт к вызывающему коду.
Начиная с Go 1.21 по умолчанию recover возвращает ненулевое значение типа *runtime.PanicNilError, что устраняет эту неоднозначность.
Изначально panic(nil) сохраняла переданное значение nil, а recover возвращала его без изменений. Это создавало проблему: результат nil был неотличим от ситуации, когда текущая горутина вообще не находится в состоянии паники.
В Go 1.21 поведение изменили для устранения этой неоднозначности: при panic(nil) runtime подставляет специальное ненулевое значение. Для временной совместимости со старым поведением предусмотрен режим GODEBUG=panicnil=1.
Обычно обработчик паники проверяет результат recover, чтобы решить, нужно ли логировать сбой, возвращать ошибку или завершать запрос с ошибкой. В старой версии Go проверка только на nil не позволяла отличить panic(nil) от отсутствия паники.
Это опасно не тем, что паника продолжится: при корректном вызове recover она будет остановлена. Риск заключается в неверной диагностике и ошибочном успешном сценарии обработки, например в отсутствии записи в журнале или метрики.
Если deferred-функция непосредственно вызывает recover во время раскрутки стека, runtime прекращает распространение паники. В Go 1.20 результатом для panic(nil) является nil, поэтому такой код не может по одному результату определить, была ли восстановлена именно эта паника:
В Go 1.20 программа напечатает паники не было, хотя panic(nil) была вызвана и остановлена. В Go 1.21 и новее при стандартной настройке будет получено ненулевое значение *runtime.PanicNilError, поэтому проверка r == nil распознает факт паники.
Нельзя безоговорочно полагаться на конкретное текстовое представление этого значения. Если обработчику важен сам факт восстановления, достаточно проверить ненулевой результат; если нужно классифицировать причину, следует проектировать собственные типы паники или явно документировать допустимые значения.
Практическое ограничение сохраняется независимо от версии: recover работает только при прямом вызове из deferred-функции той же горутины, которая паникует. Перенос вызова в обычную вспомогательную функцию или другую горутину не делает его универсальным перехватчиком.
В HTTP-сервисе на Go 1.20 middleware перехватывал паники и отправлял метрику только при ненулевом результате recover. Один обработчик использовал panic(nil) как сигнал аварийного завершения внутренней операции. Middleware останавливал панику, но не фиксировал инцидент и мог оставить вызывающий код без ожидаемого сообщения об ошибке.
Рассматривались три варианта. Первый — запретить panic(nil) соглашением команды: это просто и совместимо со старыми версиями, но не защищает от стороннего или уже существующего кода. Второй — перейти на Go 1.21: это устраняет неоднозначность, но требует проверки совместимости окружения и сборочного конвейера. Третий — использовать собственное ненулевое значение для паники: это работает в старых версиях, но требует дисциплины и не исправляет случаи произвольного panic(nil).
Выбранный вариант — обновление до Go 1.21 и одновременный запрет panic(nil) в прикладном коде. Middleware стал отличать восстановленную панику от отсутствия паники, а явные типы причин сделали диагностику предсказуемой.
recover, возвращающий nil, панику в Go 1.20?Да. Важен не только результат, но и контекст вызова: если recover вызван непосредственно из deferred-функции во время паники, он прекращает раскрутку стека даже тогда, когда возвращает nil. Ошибка заключается именно в неверной интерпретации результата, а не в продолжении паники.
panic(nil) безопасной заменой возврату error?Нет. panic предназначена для аварийного нарушения нормального управления или внутренних инвариантов, а не для обычного распространения ожидаемых отказов. panic(nil) дополнительно усложняет диагностику и поведение зависит от версии Go, поэтому для штатных ошибок следует возвращать error.
GODEBUG=panicnil=1 в Go 1.21 и новее?Будет восстановлено старое поведение: recover после panic(nil) снова вернёт nil. Код, который считает ненулевой результат единственным признаком паники, в таком режиме снова станет неоднозначным. Поэтому этот параметр следует рассматривать как средство совместимости, а не как основу нового дизайна обработки паник.