Go: как программа восстанавливается после паники?

Kate

Administrator
Команда форума
Паники (Panic) в Go запускаются, когда программа не может должным образом обработать ошибку, например, происходит недопустимый доступ к памяти. Они также могут быть инициированы разработчиком, если ошибка является неожиданной и нет другого способа справиться с ней. Понимание процесса восстановления или прекращения работы в такой ситуации может быть весьма полезно для понимания последствий запаниковавшей программы.

Несколько фреймов​

Классический пример паники и ее функции восстановления хорошо задокументированы, в том числе в блоге Go в разделе «Defer, Panic, and Recover». Но давайте сосредоточимся на другом примере, когда паника включает несколько фреймов отложенных (deferred) функций. Вот пример:

87c725e5d07bc70be0e510eacfbd867c.png

Эта программа состоит из трех функций, которые вызываются по цепочке. Как только код достигнет паники на последнем уровне, Go создаст первый фрейм отложенных функций и запустит его:

0217c80bad44f18523a971d51ef17d93.png

Код, запущенный в этом фрейме, не восстанавливает (recover) панику. Далее Go создает родительский фрейм и вызывает каждую отложенную функцию в этом фрейме:

71e8de06553e02f0ee6131e785eaa2d7.png

Напомним, что отложенные функции выполняются в порядке LIFO (last-in-first-out - последний вошел - первый вышел). Для получения дополнительной информации о способах внутреннего управления отложенными функциями я предлагаю вам почитать мою статью «Go: How Does defer Statement Work?».

Поскольку одна из функций устраняет (recover) панику, Go необходим способ отследить ее возобновить выполнение программы. Для этого в каждую горутину встроен специальный атрибут, указывающий на объект, представляющий панику:

3a827319ef446ad6947b5c6c1a1dc4a8.png

При возникновении паники этот объект создается перед запуском отложенных функций. Затем функция восстановления паники фактически просто возвращает информацию об этом объекте вместе с пометкой паники как устраненной:

04955eee721a546aa6671c4aba9acb72.png

Как только паника считается устраненной, Go необходимо возобновить текущую работу. Однако, поскольку рантайм находится среди отложенных фреймов, он не знает, где продолжить. По этой причине, когда паника помечена как устраненная, Go сохраняет текущий счетчик программы и указатель стека текущего фрейма, чтобы возобновить работу сразу после функции, в которой возникла паника:

34200d411759207ef408974a36d7ad44.png

Мы также можем проверить, что представляет собой счетчик программы, с помощью objdump (например, objdump -D my-binary | grep 105acef):

ca0085fef5b7cc1ab5d8c2f499e0ebe2.png

Эта инструкция указывает на вызов функцией runtime.deferreturn как инструкции, вставленной компилятором в конце каждой функции, которая запускает отложенную функцию. В предыдущем примере большинство из них уже были запущены (до восстановления), поэтому перед возвратом к вызывающей стороне выполняться будут только оставшиеся функции.

WaitGroup​

Понимание этого процесса раскрывает для нас важность отложенных функций и то, как они могут иметь очень большое значение, например, при работе с группой горутин. Отсрочка вызова объекта WaitGroup в отложенной функции может предотвратить взаимную блокировку. Вот пример:

76e9b0559e7e69e2cf7025b050f635d6.png

Эта программа результирует во взаимной блокировке, поскольку wg.Done никогда не вызывалась. Перемещение этого вызова в отложенную функцию гарантирует вызов и позволит программе продолжить выполнение.

Goexit​

Также интересно отметить, что функция runtime.Goexit использует точно такой же рабочий процесс. Фактически он создает объект паники со специальным флагом, чтобы отличить его от настоящей паники. Этот флаг позволяет рантайму пропустить восстановление и завершить работу должным образом, а не останавливать выполнение программы.

 
Сверху