Menu

panic и recover в Go: паники во время выполнения и когда паниковать

Паника прерывает обычное выполнение и раскручивает стек, выполняя отложенные вызовы. Что вызывает паники, как recover в отложенной функции их останавливает, какие сообщения среды выполнения вы увидите и когда паника это правильное решение.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Как выглядит паника

Паника останавливает текущую функцию, выполняет её отложенные вызовы, затем делает то же в вызывающей функции и так далее вверх по стеку. Если она доходит до вершины горутины, программа падает.

Программа завершается с кодом 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

Что произошло во втором вызове:

  1. a / b запаниковало.
  2. Выполнилось отложенное замыкание, и recover() вернул значение паники (runtime.Error).
  3. Раскрутка остановилась. 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.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ