Go: Функции

Теория: panic и recover

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

Рекомендуемые программы

+7 800 100 22 47

бесплатно по РФ

+7 495 085 21 62

бесплатно по Москве

108813 г. Москва, вн.тер.г. поселение Московский,
г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3
ОГРН 1217300010476
ИНН 7325174845