Можно ли отловить все паники с помощью recover в Go?
Что проверяют
Знаете ли вы, что recover это не аналог try/catch, и где именно проходит его граница. Следом почти всегда спрашивают, что случится с паникой в отдельной горутине и какие сбои рантайма не перехватываются вовсе.
Как отвечать
Короткий ответ: нет, не все. recover возвращает значение паники, только если вызван непосредственно из функции, отложенной через defer, и только в той горутине, где паника разворачивает стек. Дальше по пунктам:
- вызов не из
defer:recover()в обычном коде всегда возвращаетnil, паника летит дальше. Не сработают ниdefer recover(), ниdefer func() { helper() }()сrecover()внутриhelper: вызов обязан стоять прямо в отложенной функции; - чужая горутина: паника в
go func()без собственногоdeferкладёт весь процесс, родительскийrecoverеё не увидит. Отсюда правило: каждая долгоживущая горутина получает своё восстановление и пишет ошибку в канал; - фатальные ошибки рантайма: это не паники, а
fatal error, и они не перехватываются. Конкурентная запись в map, переполнение стека, блокировка всех горутин сразу. При этом разыменование nil и выход за границы среза обычные паники, они восстанавливаются; os.Exitиruntime.Goexit: первый завершает процесс мимо всехdefer, второй завершает горутину, отложенные функции выполнятся, ноrecoverвернётnil;panic(nil): начиная с Go 1.21 превращается в*runtime.PanicNilError, иrecoverотдаёт неnil. Старое поведение включается черезGODEBUG=panicnil=1.
Полезно назвать, где восстановление уместно: на границе процесса, в HTTP-обработчике (net/http сам восстанавливает панику соединения, кроме ErrAbortHandler), в gRPC-интерсепторе. Внутри библиотечной функции панику не глотают, а превращают в error.
Где спотыкаются
Ставят один defer с recover в main и считают сервис защищённым. Паника в фоновой горутине уронит процесс мимо этого места.
Пишут defer recover(). Код компилируется и выглядит рабочим ровно до первой паники.
Восстанавливаются молча. После recover состояние может быть неконсистентным, поэтому логируйте значение и debug.Stack(), а потом решайте, продолжать или гасить компонент.