Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 3 из 25 ~20 мин +50 XP

Паника и восстановление

Паника в Go — аварийная остановка, после которой программа по умолчанию не выживает, а recover позволяет перехватить её в отложенной функции и продолжить работу. Разберём, чем паника отличается от ошибки, как работает раскрутка стека и когда паниковать уместно: все ключевые примеры запускаются прямо на странице.

Содержание урока · 11 разделов

Паника — это не ошибка#

В Go две аварийные механики, и путать их дорого. Ошибка — значение типа error: функция вернула «не получилось», а вы решаете, что с этим делать. Мы разбирали эту идиому в уроке про ошибки. Паника — сломанная инвариантность: программа оказалась в состоянии, для которого у неё нет кода. Обращение по индексу за границей среза, разыменование nil, вызов метода по нулевому интерфейсу — runtime поднимает панику сам, а человек может сделать то же самое через panic(значение).

Что происходит при панике: выполнение текущей функции прекращается, начинают раскручиваться отложенные вызовы defer по всей цепочке вложенных функций, в stderr печатается трассировка, программа завершается с кодом возврата 2. Если на пути раскрутки ни одна отложенная функция не вызвала recover — спасти уже ничего нельзя: процесс умрёт целиком, вместе со всеми горутинами.

Паника вручную

GO
package main

import "fmt"

func main() {
        fmt.Println("до")
        panic("что-то пошло не так")
        fmt.Println("после")
}

После panic выполнение main останавливается: строка «после» не напечатается никогда. Песочница сообщит об ошибке «panic: что-то пошло не так» — в настоящем Go к этому добавится трассировка стека и код возврата 2. В panic можно передать что угодно: строку, ошибку, даже число.

Раскрутка стека: отложенные вызовы срабатывают#

Паника не отменяет уборку. При раскрутке выполняются все defer — начиная с текущей функции и вверх по цепочке вызовов. Это вторая, неочевидная причина писать cleanup через defer: он срабатывает даже в аварийном сценарии. Файл закроется, блокировка отпустится, счётчик активных запросов уменьшится — а уже потом процесс умрёт.

Это же делает панику «вежливее» мгновенных способов умереть: os.Exit обрывает процесс на месте, и отложенная уборка пропускается целиком.

Проверим на живом примере: функция risky паникует, но её отложенная уборка успевает напечататься. А вот код после вызова risky() в main — уже нет: панику никто не перехватил, и дальше раскрутка идёт до вершины.

Уборка перед смертью

GO
package 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: без неё обработчик сработает на каждом нормальном выходе из функции.

Ловим панику

GO
package 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 из этого урока делают ровно то же самое — теперь вы умеете их писать с нуля.

Достройте обёртку

Впишите вызов так, чтобы обёртка получала значение паники и печатала его. Без паники обёртка должна молчать.

package main import "fmt" func guard(fn func()) { defer func() { if r := ; r != nil { fmt.Println("восстановились:", r) } }() fn() } func main() { guard(func() { panic("сбой") }) fmt.Println("работаем дальше") }

Соберите канонический обработчик

Соберите тело отложенного замыкания, которое перехватывает панику: проверка, печать, закрытие ветки.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Ошибка — значение error; паника — аварийная остановка с раскруткой стека и кодом возврата 2.
  • При панике выполняются все defer по цепочке вызовов: уборка срабатывает даже в аварии.
  • recover работает только внутри отложенной функции и возвращает значение паники — или nil.
  • После recover функция возвращается нормально, программа продолжает работу.
  • Паника в горутине не перехватывается извне: каждая горутина оборачивает себя сама.
  • Невалидный ввод — ошибка; сломанный инвариант вашего кода — паника. os.Exit пропускает уборку.

Домашнее задание

Напишите функцию parseAmount(s string) (result int), которая паникует на пустой строке, и вызывающий код, перехватывающий панику через defer с recover и печатающий понятное сообщение. Затем перепишите то же самое через возврат ошибки с fmt.Errorf и сравните, какой вариант удобнее вызывать. Второй окажется — это и есть практический ответ на вопрос «когда паниковать».

Личные заметкиПусто
Хранятся только в вашем браузере (Go Pro · Паника и восстановление)

Вопросы и ответы по уроку

Чем panic отличается от os.Exit?

os.Exit завершает процесс немедленно: defer не выполняются, recover не срабатывает. Паника сначала раскручивает стек — отложенные вызовы успевают сработать, и перехват возможен. Для сценария «убрать за собой и умереть» паника вежливее.

Можно ли перехватить панику одной горутины из другой?

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

Что вернёт recover(), если паники не было?

nil. Поэтому проверка r != nil в defer-обработчике обязательна: без неё вы будете «обрабатывать» несуществующую панику на каждом нормальном выходе из функции и засорять логи ложными сообщениями.

Термины урока

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

Следующая ступень учебного пути

Fiber25 уроков~13 чпрактика25 уроков уже доступны

Fiber: экспресс-подобный веб-фреймворк на Fasthttp

Курс Fiber для Go: маршруты, middleware, JSON, CORS, WebSocket, шаблоны, лимитеры и тестирование. 25 уроков с примерами кода и итоговым REST API.

  • Поднимать HTTP-сервер на Fiber за минуты
  • Проектировать маршруты, группы и версии API
  • Писать middleware: логи, CORS, лимиты, recovery
  • Принимать и отдавать JSON с валидацией