Как выглядит паника
Паника останавливает текущую функцию, выполняет её отложенные вызовы, затем делает то же в вызывающей функции и так далее вверх по стеку. Если она доходит до вершины горутины, программа падает.
Программа завершается с кодом 2. Вывод:
before
deferred in main: still runs
panic: runtime error: index out of range [5] with length 3
goroutine 1 [running]:
main.main()
/tmp/main.go:11 +0x...
exit status 2
Отложенный вызов выполнился до отчёта о падении. В трассировке указаны горутина, функция и строка, и обычно этого достаточно, чтобы найти баг.
Типичные паники во время выполнения
| Сообщение | Причина |
|---|---|
index out of range [5] with length 3 | индекс слайса, массива или строки за концом |
slice bounds out of range [:7] with capacity 5 | срез за пределами ёмкости |
invalid memory address or nil pointer dereference | чтение поля или вызов через nil-указатель |
assignment to entry in nil map | запись в мапу, которую так и не создали |
interface conversion: interface {} is int, not string | утверждение типа с одним значением к не тому типу |
integer divide by zero | целочисленное деление или остаток от деления на 0 (float вместо этого даёт +Inf или NaN) |
close of closed channel, send on closed channel | неправильное использование канала |
all goroutines are asleep - deadlock! | все горутины заблокированы (фатальная ошибка, а не паника) |
Каждый из этих случаев это баг в программе, а не ситуация, которую нужно обрабатывать. Исправление это проверка границ, проверка на nil, make или утверждение comma-ok, а не recover.
Восстановление
recover() останавливает панику. Он работает, только если вызван непосредственно внутри отложенной функции, потому что отложенные функции это единственный код, который выполняется во время раскрутки паники.
Вывод:
5 <nil>
0 recovered: runtime error: integer divide by zero
program continues
Что произошло во втором вызове:
a / bзапаниковало.- Выполнилось отложенное замыкание, и
recover()вернул значение паники (runtime.Error). - Раскрутка остановилась.
safeDivideнормально вернула управление вmain, а именованный результатerrустановило замыкание.
Именно именованный результат позволяет отложенной функции вернуть ошибку. Без него функция возвращает свои нулевые значения. Как отложенные замыкания меняют результаты, разобрано на странице о defer.
recover() возвращает nil, когда паники нет, поэтому проверка if r != nil делает отложенную функцию безвредной на обычном пути. Вызванный вне отложенной функции или в функции, которую вызывает отложенная функция, recover возвращает nil и ничего не делает.
panic со своим значением
panic принимает любое значение. Обычно это ошибка или строка.
Перехватывайте то, что ожидаете, а всё остальное паникуйте заново. Если глотать все паники, настоящие баги окажутся скрыты.
Начиная с Go 1.21 panic(nil) превращается в *runtime.PanicNilError, так что nil из recover() теперь надёжно означает «паники не было».
Паники в горутинах
recover перехватывает паники только в своей горутине. Паника в любой горутине без recover убивает весь процесс, включая main и все остальные горутины.
Две строки воркеров могут напечататься в любом порядке; main finished всегда последняя. defer recover() в main не спас бы программу от второго воркера. Поэтому HTTP-серверы восстанавливаются на уровне запроса: net/http перехватывает паники в горутине каждого обработчика, логирует их и закрывает это соединение, так что один плохой запрос не роняет сервер.
Некоторые сбои это фатальные ошибки, а не паники, и восстановиться после них нельзя вовсе: concurrent map writes, нехватка памяти и сообщение детектора взаимных блокировок all goroutines are asleep.
Когда паника это правильное решение
Общее правило Go: возвращайте ошибки для всего, что может пойти не так во время выполнения, и паникуйте только из-за ошибок программиста. Конкретно паника уместна, когда:
- Нарушен инвариант.
switchпо вашему собственному перечислению попадает в вариант, который невозможен. Продолжение испортило бы данные. - Помощник
Mustполучил некорректный константный ввод.regexp.MustCompile,template.Mustиuuid.MustParseоборачивают функцию, возвращающую ошибку, и паникуют при неудаче. Используйте их для значений, известных на этапе компиляции, обычно для переменных уровня пакета, где сбой означает, что неправилен исходный код:
var emailRE = regexp.MustCompile(`^[^@\s]+@[^@\s]+$`)
- Запуск не может продолжаться. В
mainнет обязательной конфигурации. Даже здесь напечатать ошибку и вызватьos.Exit(1)часто аккуратнее, чем показывать трассировку стека.
Паника не подходит для:
- Ожидаемых сбоев: неверный пользовательский ввод, отсутствующий файл, таймаут. Возвращайте
error; см. обработку ошибок. - Управления потоком: panic и recover в роли исключений через большое дерево вызовов делают код трудным для понимания. Стандартная библиотека делает так внутри себя в паре мест (кодировщик
encoding/json), но всегда восстанавливается до возврата, так что ни одна паника не покидает пакет. - API библиотек: библиотека, которая паникует на плохом вводе, заставляет каждого вызывающего добавлять recover. Возвращайте ошибку.
Частые ошибки
- Вызов recover вне отложенной функции. Он возвращает
nil. - recover в
mainдля паники в горутине. Каждой горутине нужен свой. - Проглатывание всех паник. Логируйте со стеком (
debug.Stack()изruntime/debug) и паникуйте заново то, чего не ожидали. recoverдля записи в nil-мапу или индекса за границами. Исправьте баг.
Часто задаваемые вопросы
Что такое паника в Go?
Сбой во время выполнения, который прерывает обычный поток текущей горутины. Go выполняет отложенные вызовы каждой функции на стеке, от самой внутренней наружу, и если ничто не восстановит выполнение, программа печатает значение паники и трассировку стека и завершается с кодом 2. Паники возникают из-за багов (индекс за границами, разыменование nil-указателя, запись в nil-мапу) или из-за явного вызова panic(v).
Как восстановиться после паники в Go?
Вызовите recover() внутри отложенной функции: defer func() { if r := recover(); r != nil { ... } }(). Он возвращает значение, переданное в panic, и останавливает раскрутку, так что функция, которая его отложила, нормально возвращает управление вызывающему коду. В любом другом месте recover возвращает nil и ничего не делает.
Можно ли перехватить панику из другой горутины?
Нет. recover останавливает панику только в той горутине, где он выполняется. Паника в запущенной вами горутине, внутри которой нет recover, роняет всю программу. Каждой горутине, которая может запаниковать, нужен свой отложенный recover.
Когда использовать panic вместо возврата ошибки?
Для багов и невозможных состояний, а не для ожидаемых сбоев. Неверный ввод, отсутствующие файлы и сетевые ошибки это ошибки. Паника уместна, когда нарушен инвариант, когда помощнику Must передали константу, которая всегда должна быть корректной (regexp.MustCompile), или когда программа вообще не может запуститься. Библиотеки не должны выпускать паники за пределы своего публичного API.