---
title: "Паника и восстановление"
description: "panic и recover в Go: раскрутка стека, recover внутри defer, когда паника уместна и чем она отличается от ошибок. Живые примеры в браузерной песочнице."
course: "Продвинутые возможности Go: от замыканий до рефлексии"
url: "https://golangmaster.ru/courses/go-advanced/lessons/03-panic-recover/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 20
---

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

*Курс «Продвинутые возможности Go: от замыканий до рефлексии», урок 3 из 25. ~20 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/go-advanced/lessons/03-panic-recover/*

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

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

В Go две аварийные механики, и путать их дорого. Ошибка — значение типа error: функция вернула «не получилось», а вы решаете, что с этим делать. Мы разбирали эту идиому в [уроке про ошибки](/courses/go-basics/lessons/21-oshibki-i-vozvraty/). Паника — сломанная инвариантность: программа оказалась в состоянии, для которого у неё нет кода. Обращение по индексу за границей среза, разыменование 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 работает только из отложенной функции. Верните обёртку и попробуйте паниковать на третьем запросе тоже.

```go
package main

import "fmt"

func handle(i int) {
        defer func() {
                if r := recover(); r != nil {
                        fmt.Println("запрос", i, "упал:", r)
                }
        }()
        fmt.Println("запрос", i, "начат")
        if i == 2 {
                panic("битый запрос")
        }
        fmt.Println("запрос", i, "готов")
}

func main() {
        for i := 1; i <= 3; i++ {
                handle(i)
        }
        fmt.Println("сервер жив")
}
```

### Проверьте себя

**Вопрос:** Где вызов recover() реально перехватывает панику?

- В любом месте функции
- ✅ Только внутри отложенной (defer) функции
- Только в main
- Внутри горутины, вызвавшей панику

Механизм рассчитан на раскрутку: перехват возможен лишь в отложенной функции, которую паника и выполняет на своём пути. В обычном коде recover() возвращает nil и ничего не делает.

### Что выведет код?

```go
package main

import "fmt"

func main() {
        defer func() {
                if r := recover(); r != nil {
                        fmt.Println("паника:", r)
                } else {
                        fmt.Println("паники не было")
                }
        }()
        fmt.Println("всё спокойно")
}
```

**Что выведет программа?**

- ✅ всё спокойно, затем паники не было
- всё спокойно, затем паника: nil
- только всё спокойно
- паника: nil, затем всё спокойно

Паники не было — recover() вернул nil, сработала ветка else. Это не ошибка: такая проверка в defer — нормальный способ узнать, чем закончилась функция, особенно в обёртках с логированием.

### Паника поверх паники и горутины

Что будет, если паниковать внутри обработчика recover? Раскрутка начнётся заново — уже с новым значением, — и перехватить её можно только на следующем уровне, где снова есть defer. Это основа паттерна «перехватил — не умею чинить — пробрось»: middleware логирует панику и метрики, а затем паникует дальше, если восстановить работу нельзя. В песочнице этот сценарий упрощён, поэтому runnable-примера не будет — проверяйте такое в настоящем Go.

Отдельная история — паника в горутине. Recover из другой горутины её не спасает: перехват возможен только внутри отложенной функции той же горутины, где произошла паника. Неперехваченная паника в любой горутине валит всю программу целиком. Отсюда правило для конкурентного кода: каждую горутину, которая выполняет чужой или потенциально опасный код, оборачивайте в recover-обёртку внутри неё самой. Подробнее — в [уроке про горутины](/courses/go-advanced/lessons/04-gorutiny/).

### Две ошибки новичка

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

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

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

```go
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("работаем дальше")
}
```

Подсказка: Встроенная функция без аргументов: возвращает значение паники — или nil, если паники не было.

Правильные ответы в пропусках: `recover`

> **Не паникуйте на данных.** Проверка пароля, валидация формы, отсутствие записи в базе — всё это ошибки, а не panic. Паника на пользовательских данных в библиотеке превращает обрабатываемую ситуацию в падение всего процесса. Паника уместна, когда нарушен контракт вашего собственного кода — и тогда честнее упасть, чем вернуть бессмыслицу.

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

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

Правильный порядок строк:

1. `defer func() {`
2. `	if r := recover(); r != nil {`
3. `		fmt.Println("поймали:", r)`
4. `	}`
5. `}()`

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

- Ошибка — значение 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-обработчике обязательна: без неё вы будете «обрабатывать» несуществующую панику на каждом нормальном выходе из функции и засорять логи ложными сообщениями.
