- Что такое паника
- Инварианты: когда всё ломается
- Синтаксис panic
- recover: как перехватить панику
- Как это работает по шагам
- Если нет defer
- Несколько defer
- Как это работает на практике
В нормальной жизни программы ошибки — это обычное дело. В Go для этого есть проверка error
. Мы открываем файл — его может не быть. Отправляем запрос — сервер может не ответить. Это ожидаемые ситуации, и для них есть явная проверка:
f, err := os.Open("file.txt")
if err != nil {
fmt.Println("ошибка:", err)
return
}
defer f.Close()
Но бывают ситуации, которые не должны происходить никогда. Если они произошли — значит мир сломался, программа в опасном состоянии. Это и есть место для panic
.
Что такое паника
panic
— это встроенная функция, которая принимает значение любого типа и запускает аварийный выход. После вызова panic
Go начинает «сворачивать» стек вызовов: выходит из функции, выполняет все её defer
, поднимается выше и так до самого main
. Если за это время никто не перехватит панику, программа завершится с трассировкой.
Почему слово «паника»? Представь пожарную сигнализацию. Когда она сработала, уже неважно, чем ты занимался: нужно бросать всё и эвакуироваться. В Go то же самое: panic
— это сигнал о критическом сбое.
Инварианты: когда всё ломается
Чтобы понять, зачем нужен panic
, важно знать про инварианты.
Инвариант — это правило, которое должно выполняться всегда. Если оно нарушается — программа становится некорректной.
- Математика. 2 × 2 всегда равно 4. Если вдруг получилось 5 — мир поломался.
- Срезы. Индекс должен быть меньше длины.
- Деление. Нельзя делить на ноль.
Примеры в Go:
nums := []int{1, 2, 3}
fmt.Println(nums[5]) // panic: runtime error: index out of range
a, b := 10, 0
fmt.Println(a / b) // panic: runtime error: integer divide by zero
В этих случаях возвращать error
нет смысла: программа находится в невалидном состоянии. Это именно случай для panic
.
Синтаксис panic
panic(value)
value
может быть строкой, числом, структурой или error
. Чаще всего используют строку или error
.
Пример:
func main() {
fmt.Println("Перед паникой")
panic("Что-то пошло не так!")
fmt.Println("Эта строка никогда не выполнится")
}
Вывод:
Перед паникой
panic: Что-то пошло не так!
...
exit status 2
recover: как перехватить панику
Когда в коде срабатывает panic
, Go включает «режим аварии» и начинает разматывать стек вызовов. На каждом уровне выполняются все defer
, объявленные в этой функции. Именно в этот момент и появляется шанс остановить аварию.
recover()
работает только внутри defer
. Причина простая: только в момент выполнения отложенной функции рантайм находится в «паническом» состоянии. Если вызвать recover
где-то в теле функции напрямую, он всегда вернёт nil
— паники ведь ещё нет.
Если паника случилась глубоко в коде, без recover
процесс просто упадёт. Иногда это нормально (инициализация без ресурса). Но часто лучше не валить всё приложение, а аккуратно перехватить аварию, залогировать и продолжить.
Пример: HTTP-сервер. Один обработчик упал в панику → без recover
весь сервер умер. С recover
сервер вернёт 500 для конкретного запроса, а остальные запросы продолжат работать.
Минимальный шаблон:
defer func() {
if r := recover(); r != nil {
fmt.Println("Паника перехвачена:", r)
}
}()
Почему именно так? Потому что только во время выполнения defer
рантайм находится в «паническом» состоянии. Если вызвать recover
напрямую, он всегда вернёт nil
.
Как это работает по шагам
Пример:
func crash() {
fmt.Println("Начало crash")
panic("сломалось всё")
fmt.Println("Конец crash") // не дойдём
}
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Перехватили:", r)
}
}()
crash()
fmt.Println("main продолжает работу")
}
Вывод:
Начало crash
Перехватили: сломалось всё
main продолжает работу
Шаги:
- В
crash
вызываемpanic
. - Go выходит из
crash
. Там нетrecover
→ идём выше. - В
main
естьdefer
, он срабатывает. recover
перехватывает панику, возвращает её значение.- Разматывание стека останавливается, выполнение идёт дальше.
Если нет defer
func main() {
fmt.Println("Начало")
panic("авария")
fmt.Println("Конец") // недостижимо
}
Вывод:
Начало
panic: авария
...
exit status 2
Без defer
панику перехватить негде, программа падает.
Несколько defer
Даже если паника перехвачена, все остальные отложенные вызовы всё равно выполнятся в LIFO-порядке.
func demo() {
defer fmt.Println("cleanup #1")
defer func() {
if r := recover(); r != nil {
fmt.Println("перехват:", r)
}
}()
defer fmt.Println("cleanup #2")
panic("бум")
}
Вывод:
cleanup #2
перехват: бум
cleanup #1
Как это работает на практике
Без recover: стек идёт до самого верха и программа падает
package main
import "fmt"
func main() {
fmt.Println("main: start")
defer fmt.Println("main: defer")
A()
fmt.Println("main: end") // недостижимо
}
func A() {
fmt.Println("A: start")
defer fmt.Println("A: defer")
B()
fmt.Println("A: end") // недостижимо
}
func B() {
fmt.Println("B: start")
defer fmt.Println("B: defer")
C()
fmt.Println("B: end") // недостижимо
}
func C() {
fmt.Println("C: start")
defer fmt.Println("C: defer")
panic("boom")
}
Вывод:
main: start
A: start
B: start
C: start
C: defer
B: defer
A: defer
main: defer
panic: boom
...
exit status 2
Стек полностью свернулся: сначала defer
из C
, потом из B
, потом из A
, потом из main
. После этого программа упала.
С recover в main: паника перехватывается и программа живёт дальше
package main
import "fmt"
func main() {
fmt.Println("main: start")
defer func() {
if r := recover(); r != nil {
fmt.Println("main: recover ->", r)
}
}()
defer fmt.Println("main: defer")
A()
fmt.Println("main: end") // теперь достижимо
}
func A() {
fmt.Println("A: start")
defer fmt.Println("A: defer")
B()
fmt.Println("A: end") // недостижимо
}
func B() {
fmt.Println("B: start")
defer fmt.Println("B: defer")
C()
fmt.Println("B: end") // недостижимо
}
func C() {
fmt.Println("C: start")
defer fmt.Println("C: defer")
panic("boom")
}
Вывод:
main: start
A: start
B: start
C: start
C: defer
B: defer
A: defer
main: recover -> boom
main: defer
main: end
Разница очевидна: паника дошла до main
, но там в defer
сработал recover()
. Разматывание стека остановилось, и программа продолжила работу — строка main: end
выполнилась.
Самостоятельная работа
Эта задача помогает понять, как корректно перехватывать панику и продолжать выполнение программы, не падая целиком.
Реализуйте функцию SafeIndex(xs []int, i int) (val int, ok bool)
, которая пытается вернуть xs[i]
. Если при обращении к элементу произойдёт паника (выход за границы), функция должна перехватить её через recover
и вернуть 0, false
. В случае успеха вернуть значение и true
.
Пример использования:
xs := []int{10, 20}
v1, ok1 := SafeIndex(xs, 1) // 20, true
v2, ok2 := SafeIndex(xs, 5) // 0, false
fmt.Println(v1, ok1)
fmt.Println(v2, ok2)
Подсказки:
- Используйте
defer
+recover
в этой функции, чтобы перехватить возможную панику. - Удобно объявить именованные возвращаемые значения и присваивать им внутри отложенной функции.
Показать решение
package main
import "fmt"
func SafeIndex(xs []int, i int) (val int, ok bool) {
defer func() {
if r := recover(); r != nil {
val = 0
ok = false
}
}()
val = xs[i] // может вызвать панику при выходе за границы
ok = true
return val, ok
}
func main() {
xs := []int{10, 20}
v1, ok1 := SafeIndex(xs, 1)
v2, ok2 := SafeIndex(xs, 5)
fmt.Println(v1, ok1) // 20 true
fmt.Println(v2, ok2) // 0 false
}
Дополнительные материалы
- The Go Blog — Defer, Panic, and Recover
- Effective Go — Panic
- Effective Go — Recover
- Go by Example — Panic
- Go by Example — Recover
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.