Содержание урока · 11 разделов
Паника — это не ошибка#
В Go две аварийные механики, и путать их дорого. Ошибка — значение типа error: функция вернула «не получилось», а вы решаете, что с этим делать. Мы разбирали эту идиому в уроке про ошибки. Паника — сломанная инвариантность: программа оказалась в состоянии, для которого у неё нет кода. Обращение по индексу за границей среза, разыменование nil, вызов метода по нулевому интерфейсу — runtime поднимает панику сам, а человек может сделать то же самое через panic(значение).
Что происходит при панике: выполнение текущей функции прекращается, начинают раскручиваться отложенные вызовы defer по всей цепочке вложенных функций, в stderr печатается трассировка, программа завершается с кодом возврата 2. Если на пути раскрутки ни одна отложенная функция не вызвала recover — спасти уже ничего нельзя: процесс умрёт целиком, вместе со всеми горутинами.
Паника вручную
GOpackage main
import "fmt"
func main() {
fmt.Println("до")
panic("что-то пошло не так")
fmt.Println("после")
}После panic выполнение main останавливается: строка «после» не напечатается никогда. Песочница сообщит об ошибке «panic: что-то пошло не так» — в настоящем Go к этому добавится трассировка стека и код возврата 2. В panic можно передать что угодно: строку, ошибку, даже число.
Раскрутка стека: отложенные вызовы срабатывают#
Паника не отменяет уборку. При раскрутке выполняются все defer — начиная с текущей функции и вверх по цепочке вызовов. Это вторая, неочевидная причина писать cleanup через defer: он срабатывает даже в аварийном сценарии. Файл закроется, блокировка отпустится, счётчик активных запросов уменьшится — а уже потом процесс умрёт.
Это же делает панику «вежливее» мгновенных способов умереть: os.Exit обрывает процесс на месте, и отложенная уборка пропускается целиком.
Проверим на живом примере: функция risky паникует, но её отложенная уборка успевает напечататься. А вот код после вызова risky() в main — уже нет: панику никто не перехватил, и дальше раскрутка идёт до вершины.
Уборка перед смертью
GOpackage main
import "fmt"
func risky() {
defer fmt.Println("cleanup в risky")
fmt.Println("работаем")
panic("бум")
}
func main() {
risky()
fmt.Println("сюда не дойдём")
}В выводе появится «cleanup в risky» — defer успел сработать в процессе раскрутки. Строки «сюда не дойдём» не будет: паника не перехвачена, и программа завершилась. Песочница покажет ошибку паники — это ожидаемый результат, а не поломка примера.
recover: перехват паники в defer#
Встроенная функция recover останавливает раскрутку и возвращает значение, переданное в panic. Работает она в единственном месте — внутри отложенной функции. Вызов recover() в обычном коде вернёт nil и ничего не изменит: механизм рассчитан именно на defer, потому что перехват должен случиться на пути раскрутки, а не где-то до или после неё.
Каноническая обёртка выглядит одинаково во всей экосистеме Go: defer func() { if r := recover(); r != nil { ... } }(). Внутри r — то, что передали в panic: строка, ошибка, любое значение. После успешного recover функция, в которой объявлен этот defer, возвращается нормально, и программа продолжает работу со следующего оператора после места, где объявлена обёртка.
Если паники не было, recover() возвращает nil — отсюда обязательная проверка r != nil: без неё обработчик сработает на каждом нормальном выходе из функции.
Ловим панику
GOpackage main
import "fmt"
func safe() {
defer func() {
if r := recover(); r != nil {
fmt.Println("поймали:", r)
}
}()
panic("бум")
}
func main() {
safe()
fmt.Println("программа продолжается")
}Паника внутри safe гасится отложенным замыканием: recover вернул «бум», функция вернулась как ни в чём не бывало, main продолжил работу. Запустите и уберите проверку r != nil — увидите, что «поймали» напечатается и без паники.
Сервер, который не падает целиком
Запустите и посмотрите, как переживается падение второго запроса. Затем уберите обёртку defer func(){...}() и оставьте внутри handle простой recover() — паника перестанет ловиться: recover работает только из отложенной функции. Верните обёртку и попробуйте паниковать на третьем запросе тоже.
Запустите код, чтобы увидеть вывод.
Проверьте себя
Где вызов recover() реально перехватывает панику?
Что выведет код?
Прочитайте код и выберите, что он выведет:
package main
import "fmt"
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("паника:", r)
} else {
fmt.Println("паники не было")
}
}()
fmt.Println("всё спокойно")
}Паника поверх паники и горутины#
Что будет, если паниковать внутри обработчика recover? Раскрутка начнётся заново — уже с новым значением, — и перехватить её можно только на следующем уровне, где снова есть defer. Это основа паттерна «перехватил — не умею чинить — пробрось»: middleware логирует панику и метрики, а затем паникует дальше, если восстановить работу нельзя. В песочнице этот сценарий упрощён, поэтому runnable-примера не будет — проверяйте такое в настоящем Go.
Отдельная история — паника в горутине. Recover из другой горутины её не спасает: перехват возможен только внутри отложенной функции той же горутины, где произошла паника. Неперехваченная паника в любой горутине валит всю программу целиком. Отсюда правило для конкурентного кода: каждую горутину, которая выполняет чужой или потенциально опасный код, оборачивайте в recover-обёртку внутри неё самой. Подробнее — в уроке про горутины.
Две ошибки новичка#
Первая — recover вне defer. Выглядит правдоподобно: «а поставлю-ка я проверку в начале функции». Никакой ошибки компиляции не будет, recover просто вернёт nil, и паника однажды уронит программу мимо этой проверки. Диагностика неприятная: код выглядит защищённым, а защиты нет. Если пишете обёртку — начинайте её со строки defer func() {.
Вторая — recover без проверки на nil. Обработчик defer func() { recover() }() безусловно «срабатывает» на каждом выходе: без паники он бесполезен, а с побочными эффектами внутри — ещё и вреден, потому что выполняется каждый раз. Каноническая форма с if r := recover(); r != nil не украшение, а рабочий код.
И третья, из жизни: перехватили панику и промолчали. Recover без лога, метрики или замены результата прячет проблему, а не решает её. Минимум — напечатать значение r с контекстом, где это случилось.
Когда паника уместна, а когда ошибка#
Пользователь прислал невалидный JSON, файл не найден, сеть отвалилась — это ошибки, а не паники: внешние условия, с которыми программа обязана уметь жить. Паника — про сломанные обещания вашего же кода: nil там, где его не может быть, невозможная ветка switch, нарушение порядка инициализации.
Мой критерий: если состояние можно обработать и продолжить — ошибка; если продолжать нечем и каждый следующий шаг только портит данные — паника. В библиотечном коде правило жёстче: паникуйте как можно реже. Каждая паника в чужом пакете — это чья-то незапланированная ночь отладки; верните error и оставьте выбор вызывающему. Хотите дать два режима — сделайте пару функций: одну с error, вторую с паникой для заведомо невозможных состояний.
Ошибка или паника: краткая шпаргалка#
| Ситуация | Что использовать |
|---|---|
| Невалидный ввод пользователя | error — штатная ситуация, программа живёт дальше |
| Файл не найден, сеть недоступна | error — внешние условия, обрабатываются явно |
| Индекс за границей среза | Паника от runtime — чините код, а не перехватывайте |
| Неисправимая ошибка в инициализации | panic в main: программе не с чем работать |
| Библиотечный код | error — вы не знаете контекст вызывающего |
Чего паника не спасёт#
Три границы механизма, о которых стоит знать заранее. Первая: os.Exit и log.Fatal умирают мгновенно — отложенные вызовы пропускаются, recover не срабатывает. Вторая: паника, поднявшаяся внутри обработчика recover во время раскрутки, начинается заново — аккуратно с побочными эффектами в самом обработчике. Третья: panic(nil) — особый случай; в новых версиях Go он превращается в искусственную ошибку runtime.PanicNilError, чтобы recover хотя бы мог отличить «настоящую» панику от пустой.
И про веб-серверы: стандартный http.Server сам ставит перехват вокруг обработчиков — упавшая паникой обработка рвёт одно соединение, а сервер продолжает слушать. Фреймворки вроде Gin добавляют собственные Recovery-middleware, чтобы логировать панику и отдавать аккуратный ответ 500. Самописные обёртки вроде protect из этого урока делают ровно то же самое — теперь вы умеете их писать с нуля.
Достройте обёртку
Впишите вызов так, чтобы обёртка получала значение паники и печатала его. Без паники обёртка должна молчать.
Проверка пароля, валидация формы, отсутствие записи в базе — всё это ошибки, а не panic. Паника на пользовательских данных в библиотеке превращает обрабатываемую ситуацию в падение всего процесса. Паника уместна, когда нарушен контракт вашего собственного кода — и тогда честнее упасть, чем вернуть бессмыслицу.
Соберите канонический обработчик
Соберите тело отложенного замыкания, которое перехватывает панику: проверка, печать, закрытие ветки.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Ошибка — значение error; паника — аварийная остановка с раскруткой стека и кодом возврата 2.
- При панике выполняются все defer по цепочке вызовов: уборка срабатывает даже в аварии.
- recover работает только внутри отложенной функции и возвращает значение паники — или nil.
- После recover функция возвращается нормально, программа продолжает работу.
- Паника в горутине не перехватывается извне: каждая горутина оборачивает себя сама.
- Невалидный ввод — ошибка; сломанный инвариант вашего кода — паника. os.Exit пропускает уборку.
Домашнее задание
Напишите функцию parseAmount(s string) (result int), которая паникует на пустой строке, и вызывающий код, перехватывающий панику через defer с recover и печатающий понятное сообщение. Затем перепишите то же самое через возврат ошибки с fmt.Errorf и сравните, какой вариант удобнее вызывать. Второй окажется — это и есть практический ответ на вопрос «когда паниковать».
Личные заметкиПусто
Вопросы и ответы по уроку
Чем panic отличается от os.Exit?
os.Exit завершает процесс немедленно: defer не выполняются, recover не срабатывает. Паника сначала раскручивает стек — отложенные вызовы успевают сработать, и перехват возможен. Для сценария «убрать за собой и умереть» паника вежливее.
Можно ли перехватить панику одной горутины из другой?
Нет. Recover работает только в отложенной функции той же горутины, где произошла паника. Неперехваченная паника в любой горутине завершает всю программу, поэтому горутины с потенциально опасным кодом оборачивают в recover-обёртку внутри себя.
Что вернёт recover(), если паники не было?
nil. Поэтому проверка r != nil в defer-обработчике обязательна: без неё вы будете «обрабатывать» несуществующую панику на каждом нормальном выходе из функции и засорять логи ложными сообщениями.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Fiber: экспресс-подобный веб-фреймворк на Fasthttp
Курс Fiber для Go: маршруты, middleware, JSON, CORS, WebSocket, шаблоны, лимитеры и тестирование. 25 уроков с примерами кода и итоговым REST API.
- Поднимать HTTP-сервер на Fiber за минуты
- Проектировать маршруты, группы и версии API
- Писать middleware: логи, CORS, лимиты, recovery
- Принимать и отдавать JSON с валидацией