сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Ошибки и рантайм Go

panic и recover в Go

panic и recover: узкая зона применения

Ошибки в 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, остальные разбираются в тренажёре.

  1. #go_panic_recover1 / 5
    Где сработает 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 { … } }() в начале функции.

  2. #go_panic_recover2 / 5
    Горутина паникует. Перехватит ли её recover в defer той функции, что её запустила?
    A)Нет: паника не пересекает границу горутины — упадёт вся программа
    B)Да: recover запускающей функции перехватывает паники порождённых горутин
    C)Да, если горутина запущена без ключевого слова go
    D)Только если горутина и её родитель делят один канал
    показать ответ и разбор
    +A)Нет: паника не пересекает границу горутины — упадёт вся программа

    // разбор: recover работает только в СВОЕЙ горутине: паника не поднимается к тому, кто запустил горутину через go. Непойманная в самой горутине паника роняет весь процесс, сколько бы recover ни стояло у родителя. Поэтому каждая долгоживущая горутина должна ловить свои паники сама (defer с recover в её теле) — особенно в серверах-обработчиках.

  3. #go_panic_recover3 / 5
    Что возвращает вызов recover(), если паники не было?
    A)Пустую строку, показывающую отсутствие сообщения о панике
    B)Ошибку с текстом о том, что перехватывать нечего
    C)nil — по нему и отличают «паники не было»
    D)false, потому что recover возвращает признак срабатывания
    показать ответ и разбор
    +C)nil — по нему и отличают «паники не было»

    // разбор: recover отдаёт значение, переданное в panic, а без паники — nil. Отсюда обычная форма: в defer пишут if r := recover(); r != nil { ... }. Второе правило — вызывать его нужно напрямую в отложенной функции: recover, спрятанный во вложенном вызове внутри defer, паники не остановит и просто вернёт nil.

  4. #go_panic_recover4 / 5
    В обработчике HTTP recover перехватил панику. Что важно сделать после этого?
    A)Повторно вызвать панику, чтобы её увидел рантайм и записал в лог
    B)Вернуть клиенту ответ и записать стек — иначе соединение повиснет без ответа
    C)Перезапустить горутину обработчика, чтобы запрос обработался повторно
    D)Сбросить состояние сервера, потому что после паники оно ненадёжно
    показать ответ и разбор
    +B)Вернуть клиенту ответ и записать стек — иначе соединение повиснет без ответа

    // разбор: Перехват сам по себе только гасит падение: обработчик прервался на середине, клиент ждёт. Middleware с recover обязано ответить 500 и записать стек через debug.Stack — без стека паника превращается в невоспроизводимую загадку. Именно так устроен встроенный recover у net/http: он не даёт процессу упасть, но и разбираться за вас не станет.

  5. #go_panic_recover5 / 5
    Что произойдёт, если внутри отложенной функции произойдёт новая паника во время обработки первой?
    A)Первая паника отменится, программа продолжит с новой в качестве текущей
    B)Обе паники будут перехвачены одним recover в вызывающей функции
    C)Программа завершится, показав обе паники в трассировке
    D)Рантайм проигнорирует вторую панику как возникшую при раскрутке стека
    показать ответ и разбор
    +C)Программа завершится, показав обе паники в трассировке

    // разбор: Новая паника в defer вытесняет прежнюю как текущую, но исходная не теряется: рантайм печатает обе, помечая предыдущую как «panic ... [recovered]» либо перечисляя цепочкой. Практический вывод — в отложенных функциях, особенно в тех, что закрывают ресурсы или логируют, не должно быть кода, способного паниковать: иначе первопричина тонет во второй ошибке.

дальше

Теорию прочитали. Навык ставится повторением

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