Паники (Panic) в Go запускаются, когда программа не может должным образом обработать ошибку, например, происходит недопустимый доступ к памяти. Они также могут быть инициированы разработчиком, если ошибка является неожиданной и нет другого способа справиться с ней. Понимание процесса восстановления или прекращения работы в такой ситуации может быть весьма полезно для понимания последствий запаниковавшей программы.
Несколько фреймов
Классический пример паники и ее функции восстановления хорошо задокументированы, в том числе в блоге Go в разделе «Defer, Panic, and Recover». Но давайте сосредоточимся на другом примере, когда паника включает несколько фреймов отложенных (deferred) функций. Вот пример:
Эта программа состоит из трех функций, которые вызываются по цепочке. Как только код достигнет паники на последнем уровне, Go создаст первый фрейм отложенных функций и запустит его:
Код, запущенный в этом фрейме, не восстанавливает (recover) панику. Далее Go создает родительский фрейм и вызывает каждую отложенную функцию в этом фрейме:
Напомним, что отложенные функции выполняются в порядке LIFO (last-in-first-out - последний вошел - первый вышел). Для получения дополнительной информации о способах внутреннего управления отложенными функциями я предлагаю вам почитать мою статью «Go: How Does defer Statement Work?».
Поскольку одна из функций устраняет (recover) панику, Go необходим способ отследить ее возобновить выполнение программы. Для этого в каждую горутину встроен специальный атрибут, указывающий на объект, представляющий панику:
При возникновении паники этот объект создается перед запуском отложенных функций. Затем функция восстановления паники фактически просто возвращает информацию об этом объекте вместе с пометкой паники как устраненной:
Как только паника считается устраненной, Go необходимо возобновить текущую работу. Однако, поскольку рантайм находится среди отложенных фреймов, он не знает, где продолжить. По этой причине, когда паника помечена как устраненная, Go сохраняет текущий счетчик программы и указатель стека текущего фрейма, чтобы возобновить работу сразу после функции, в которой возникла паника:
Мы также можем проверить, что представляет собой счетчик программы, с помощью objdump
(например, objdump -D my-binary | grep 105acef
):
Эта инструкция указывает на вызов функцией runtime.deferreturn
как инструкции, вставленной компилятором в конце каждой функции, которая запускает отложенную функцию. В предыдущем примере большинство из них уже были запущены (до восстановления), поэтому перед возвратом к вызывающей стороне выполняться будут только оставшиеся функции.
WaitGroup
Понимание этого процесса раскрывает для нас важность отложенных функций и то, как они могут иметь очень большое значение, например, при работе с группой горутин. Отсрочка вызова объекта WaitGroup в отложенной функции может предотвратить взаимную блокировку. Вот пример:
Эта программа результирует во взаимной блокировке, поскольку wg.Done
никогда не вызывалась. Перемещение этого вызова в отложенную функцию гарантирует вызов и позволит программе продолжить выполнение.
Goexit
Также интересно отметить, что функция runtime.Goexit
использует точно такой же рабочий процесс. Фактически он создает объект паники со специальным флагом, чтобы отличить его от настоящей паники. Этот флаг позволяет рантайму пропустить восстановление и завершить работу должным образом, а не останавливать выполнение программы.
Материал подготовлен в рамках курса «Golang Developer. Professional».
Всех желающих приглашаем на открытый урок «Кодогенерация в Go». На уроке вы познакомитесь с механизмом кодогенерации в Go, а еще:
- разберете понятие кодогенерации;
- рассмотрите инструмент `go generate`, а также полезные библиотеки, использующие кодогенерацию: impl, stringer, jsonenums, easyjson и пр.;
- найдете решение проблемы обобщенного программирования (generics) с помощью кодогенерации;
После занятия вы точно не будете бояться библиотек, использующих кодогенерацию.
РЕГИСТРАЦИЯ