Зарегистрируйтесь, чтобы продолжить обучение

panic и recover Go: Функции

В нормальной жизни программы ошибки — это обычное дело. В 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 продолжает работу

Шаги:

  1. В crash вызываем panic.
  2. Go выходит из crash. Там нет recover → идём выше.
  3. В main есть defer, он срабатывает.
  4. recover перехватывает панику, возвращает её значение.
  5. Разматывание стека останавливается, выполнение идёт дальше.

Если нет 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
}

Дополнительные материалы

  1. The Go Blog — Defer, Panic, and Recover
  2. Effective Go — Panic
  3. Effective Go — Recover
  4. Go by Example — Panic
  5. Go by Example — Recover

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff