Как обрабатывать паники при работе с горутинами в Go

Статья в портфолио. Написана как тестовое задание на основе видео Владимира Балуна

Разбираемся, как использовать механизм Recover, чтобы перехватывать паники в горутине и восстанавливать контроль над программой.

Содержание 

  1. Как одна горутина может «уронить» весь сервер
  2. Как перехватывать паники локально с помощью Recover
  3. Зачем нужен паттерн Never Exit

Периодически Go-разработчикам приходится работать с горутинами. Но что произойдет, если какая-то горутина внезапно запаникует? И какие еще в этом процессе есть тонкости и нюансы? Разберемся вместе.

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

В конце вас ждет бонус: ссылка на материал об утечках памяти в Go, где подробно рассказано, когда и почему они происходят и как от них защититься.

Как одна горутина может «уронить» весь сервер

    Давайте рассмотрим пример, где реализован простейший TCP-сервер. В примере я «слушаю» входящие соединения на некотором порту. Также в коде есть listener, который по сути представляет собой серверный сокет. На этом серверном сокете я вызываю Accept и жду входящие клиентские соединения. После этого появляется Connection (клиентский сокет), и я отдаю его в обработчик.

    В примере я сделал так, чтобы клиентский сокет обрабатывался в отдельной горутине. Особенность заключается в том, что этот обработчик у меня паникует.

    Чтобы узнать, перестанет ли работать все приложение или пострадает только обработчик определенного входящего соединения, я запустил сервер и послал запрос.

    В результате у меня произошла паника. Из этого делаем вывод, что паника в какой-либо горутине крашит все приложение. На выходе пользователи получают ошибки и, возможно, незавершеные операции.

    Как перехватывать паники локально с помощью Recover

      Чтобы решить эту проблему, я добавил в обработчик перехват паники. Моя задача — проверить каждый обработчик и применить к нему функцию Recover, если произойдет ошибка.

      После запуска я зафиксировал, что паника была перехвачена, но сервер продолжил функционировать. То есть проблема никак не сказалась на других пользовательских соединениях, которые исполняются на сервере. 

      Важно отметить, что вам не обязательно писать эти перехватчики вручную. Для этих целей можно использовать interceptors, middlewares и прочие инструменты.

      Можно ли поймать панику из другой горутины

      Рассмотрим еще один интересный вопрос. Представим, что я хочу попробовать перехватить панику из другой горутины. Например, запустить отдельную горутину, которая паникует, и попытаться «зарекаверить» её в родительской функции. Получится ли у меня это сделать?

      После добавления нового кода и запуска я убедился, что невозможно перехватить панику, возникшую в абсолютно другой горутине. 

      Это происходит потому, что у горутины есть свой стек. Паника начинает раскручивать стек («stack unwinding») и лететь дальше по нему до тех пор, пока не обнаружит какой-то обработчик. Она долетает только до верхушки стека конкретной горутины. Поскольку у каждой горутины свой стек исполнения, Recover в одной горутине никак не влияет на панику в другой: она долетает до верхушки своего стека, и после этого приложение завершается.

      Зачем нужен паттерн Never Exit

        И в конце рассмотрим еще один паттерн, который может быть полезен в некоторых приложениях. Это паттерн Never Exit. Он нужен, когда в приложении есть воркер, который должен работать всегда, пока активно само приложение.

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

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

        Подводя итог, можно сказать, что понимание механизмов обработки паник — это критически важный навык для создания надежных систем на Go. Мы увидели, что без должного контроля одна-единственная горутина способна нарушить работу всего приложения. Чтобы не упустить важные нюансы языка, приходите на наши курсы и интенсивы по Go. 

        В качестве бонуса рекомендую ознакомиться с материалом об утечках памяти в Go, чтобы еще глубже понимать, как обеспечить стабильность вашего кода в продакшене.


        Читайте также: Как устроен мой навайбкоденный бот для игры в Крокодила