panic и recover в Go
Ошибки в Go - значения, и panic их не заменяет. Он существует для другого: для ситуаций, в которых продолжать исполнение бессмысленно, потому что нарушен инвариант самой программы. Индекс за границей слайса, разыменование nil, деление на ноль - вот его территория.
Стержень: panic - для программных ошибок и невосстановимых состояний; recover работает только в отложенной функции и только в своей горутине.
// Формулировки: «когда уместен panic?», «где сработает recover?», «почему паника в горутине роняет весь сервис?»
Где место panic
Рантайм паникует сам, когда программа делает невозможное: выход за границы слайса, разыменование nil-указателя, деление на ноль, запись в nil-мапу. Это сигнал об ошибке в коде, а не о плохом вводе.
Программисту panic уместен ровно в двух местах. Первое - при инициализации, когда без чего-то работать нельзя: не разобрался конфиг, не скомпилировался обязательный шаблон, не подключилась база на старте. Падать при запуске лучше, чем работать наполовину; на этом и построены функции с суффиксом Must - MustCompile, MustParse.
Второе - нарушение внутреннего инварианта библиотеки, до которого не должно доходить в принципе. Если пользователь вызвал метод в состоянии, которого по контракту не бывает, паника честнее тихой порчи данных.
// Чего panic делать не должен: обрабатывать плохой пользовательский ввод, отсутствующую запись в базе, обрыв сети. Это ожидаемые события, для них есть error. Паника, вылезающая из публичного API пакета на нормальном вводе, - дефект пакета.
Как работает recover
При панике исполнение функции прекращается, но отложенные вызовы выполняются - на этом всё и держится. Если внутри отложенной функции позвать recover, паника останавливается, она возвращает переданное в panic значение, и функция завершается нормально.
Условие жёсткое: recover обязан быть вызван непосредственно в теле отложенной функции. Проверено: если вынести его во вспомогательную функцию, вызванную из отложенной, recover вернёт nil, и процесс упадёт как ни в чём не бывало. Отладка такого случая занимает вечер, если не знать правила.
Второе ограничение важнее первого: recover ловит панику только своей горутины. Никакой перехват в main не спасёт от паники в запущенной горутине - проверено, процесс падает целиком с трассировкой той горутины, где рвануло.
// Отсюда практика: у каждой горутины, обрабатывающей внешние данные, свой defer с recover внутри неё самой. Одно необработанное сообщение из очереди не должно ронять весь сервис.
defer func() { fmt.Println("поймали:", recover()) }() // работает
// а так - НЕ работает:
helper := func() { fmt.Println(recover()) } // вернёт nil
defer func() { helper() }()
// паника пойдёт дальше и уронит процессПерехватили, и что дальше
Перехват - не отмена проблемы. Программа побывала в состоянии, которое автор считал невозможным, и часть работы осталась незавершённой. Продолжать как ни в чём не бывало нельзя.
Разумный сценарий один: остановить текущую единицу работы, записать в лог значение паники и стек через debug.Stack, вернуть наверх обычную ошибку. Именно так устроен веб-фреймворк с восстановлением: паника в обработчике превращается в пятисотый ответ, а сервер продолжает жить.
Отдельно про границу пакета. Библиотека может использовать panic внутри себя как быстрый выход из глубокой рекурсии, но обязана перехватить его на границе и вернуть пользователю обычную ошибку. Паника, вылетевшая наружу, - нарушение контракта.
// И маленькая, но важная деталь: паникуй значениями ошибок, а не строками. panic(fmt.Errorf(...)) даёт recover типизированное значение, с которым можно работать, а panic("что-то сломалось") - только текст.
- debug.Stack
- возвращает стек текущей горутины строкой; главное, что нужно записать при перехвате паники
Как отвечать: «Когда уместен panic и где сработает recover?»
panic уместен там, где продолжать бессмысленно, потому что нарушен инвариант программы. Практически это два места: инициализация, когда без ресурса работать нельзя - отсюда функции с Must вроде regexp.MustCompile, - и нарушение внутреннего контракта библиотеки, до которого не должно доходить. Ожидаемые события - плохой ввод, отсутствие записи, обрыв сети - это error, а не паника. Про recover два правила, и оба легко проверить. Первое: он обязан вызываться непосредственно в теле отложенной функции. Если спрятать его во вспомогательную функцию, вызванную из defer, он вернёт nil и паника пойдёт дальше - я на это натыкался. Второе, более важное: recover ловит панику только своей горутины. Перехват в main не спасёт от паники в запущенной горутине, процесс упадёт целиком. Поэтому у каждой горутины, обрабатывающей внешние данные, свой defer с recover. И перехват для меня не означает «всё в порядке»: я останавливаю текущую единицу работы, пишу в лог значение и стек через debug.Stack и возвращаю наверх обычную ошибку.
Зона применения очерчена двумя конкретными случаями, названы оба ограничения recover с проверкой, и показано, что перехват - это переход к штатной обработке, а не игнорирование.
На чём валят
- −Используют panic вместо error для плохого ввода и сетевых сбоев.
- −Зовут recover не напрямую в defer, а во вложенной функции - он возвращает nil.
- −Рассчитывают перехватить в main панику из горутины - процесс падает целиком.
- −Перехватывают панику и продолжают работу, будто ничего не случилось.
- −Выпускают панику наружу из публичного API пакета.
- −Паникуют строкой вместо значения ошибки и теряют возможность разобрать причину.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 6, остальные разбираются в тренажёре.
- Где сработает recover(), чтобы перехватить панику?A)Прямо в теле функции, там где ожидается паникаB)В блоке if сразу после потенциально паникующего вызоваC)В отдельной горутине, запущенной перед потенциально паникующим кодомD)Только в функции, вызванной через defer — иначе recover вернёт nil
показать ответ и разбор
+D)Только в функции, вызванной через defer — иначе recover вернёт nil// разбор: recover() перехватывает панику, ТОЛЬКО если вызван непосредственно в функции, отложенной через defer, — потому что defer'ы и выполняются во время разворачивания стека. Вызванный не из defer (или из вложенной обычной функции), recover вернёт nil и панику не остановит. Идиома: defer func(){ if r := recover(); r != nil { … } }() в начале функции.
- Горутина паникует. Перехватит ли её recover в defer той функции, что её запустила?A)Нет: паника не пересекает границу горутины — упадёт вся программаB)Да: recover запускающей функции перехватывает паники порождённых горутинC)Да, если горутина запущена без ключевого слова goD)Только если горутина и её родитель делят один канал
показать ответ и разбор
+A)Нет: паника не пересекает границу горутины — упадёт вся программа// разбор: recover работает только в СВОЕЙ горутине: паника не поднимается к тому, кто запустил горутину через go. Непойманная в самой горутине паника роняет весь процесс, сколько бы recover ни стояло у родителя. Поэтому каждая долгоживущая горутина должна ловить свои паники сама (defer с recover в её теле) — особенно в серверах-обработчиках.
- Что возвращает вызов recover(), если паники не было?A)Пустую строку, показывающую отсутствие сообщения о паникеB)Ошибку с текстом о том, что перехватывать нечегоC)nil — по нему и отличают «паники не было»D)false, потому что recover возвращает признак срабатывания
показать ответ и разбор
+C)nil — по нему и отличают «паники не было»// разбор: recover отдаёт значение, переданное в panic, а без паники — nil. Отсюда обычная форма: в defer пишут if r := recover(); r != nil { ... }. Второе правило — вызывать его нужно напрямую в отложенной функции: recover, спрятанный во вложенном вызове внутри defer, паники не остановит и просто вернёт nil.
- В обработчике HTTP recover перехватил панику. Что важно сделать после этого?A)Повторно вызвать панику, чтобы её увидел рантайм и записал в логB)Вернуть клиенту ответ и записать стек — иначе соединение повиснет без ответаC)Перезапустить горутину обработчика, чтобы запрос обработался повторноD)Сбросить состояние сервера, потому что после паники оно ненадёжно
показать ответ и разбор
+B)Вернуть клиенту ответ и записать стек — иначе соединение повиснет без ответа// разбор: Перехват сам по себе только гасит падение: обработчик прервался на середине, клиент ждёт. Middleware с recover обязано ответить 500 и записать стек через debug.Stack — без стека паника превращается в невоспроизводимую загадку. Именно так устроен встроенный recover у net/http: он не даёт процессу упасть, но и разбираться за вас не станет.
- Что произойдёт, если внутри отложенной функции произойдёт новая паника во время обработки первой?A)Первая паника отменится, программа продолжит с новой в качестве текущейB)Обе паники будут перехвачены одним recover в вызывающей функцииC)Программа завершится, показав обе паники в трассировкеD)Рантайм проигнорирует вторую панику как возникшую при раскрутке стека
показать ответ и разбор
+C)Программа завершится, показав обе паники в трассировке// разбор: Новая паника в defer вытесняет прежнюю как текущую, но исходная не теряется: рантайм печатает обе, помечая предыдущую как «panic ... [recovered]» либо перечисляя цепочкой. Практический вывод — в отложенных функциях, особенно в тех, что закрывают ресурсы или логируют, не должно быть кода, способного паниковать: иначе первопричина тонет во второй ошибке.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.