panic и recover
Разбираем, чем panic отличается от обычной ошибки, когда его действительно стоит использовать и как recover ловит панику, пока программа не рухнула.
panic — это встроенная функция Go, которая немедленно останавливает нормальное выполнение текущей горутины и начинает «раскручивать» стек вызовов вверх, пока её не поймает
recoverили пока программа не завершится с ошибкой.
Что выведет этот код?
Посмотри на пример и попробуй угадать, что окажется в консоли, прежде чем читать разбор.
package main
import "fmt"
func bezopasnoeDelenie(a, b int) (result int) {
defer func() {
if r := recover(); r != nil {
fmt.Println("Перехвачена паника:", r)
result = 0
}
}()
result = a / b
return result
}
func main() {
fmt.Println(bezopasnoeDelenie(10, 2))
fmt.Println(bezopasnoeDelenie(10, 0))
fmt.Println("Программа продолжает работать")
}Многие новички ожидают, что деление на ноль в целых числах вызовет обычную ошибку, как в примере из прошлого урока. Но деление целых чисел на ноль в Go — это не error, а именно паника рантайма.
Вывод:
5
Перехвачена паника: runtime error: integer divide by zero
0
Программа продолжает работатьКогда действительно нужен panic
Важно сразу разделить два мира: обычные, ожидаемые сбои (файл не найден, сервер не ответил, пользователь ввёл не число) — это работа для error, о которой шла речь в прошлом уроке. А panic предназначен для ситуаций, которые сигнализируют о настоящем программистском баге или о состоянии, из которого программа физически не может продолжать работу корректно.
Классические примеры оправданного panic: обращение по индексу за пределами слайса, разыменование nil-указателя, деление целых чисел на ноль (как в примере выше) — всё это рантайм генерирует сам, автоматически. Из своего кода panic стоит вызывать вручную довольно редко — например, при ошибке инициализации программы, без которой дальнейшая работа вообще бессмысленна (не удалось прочитать обязательный конфиг при старте сервера), или чтобы защититься от ситуации, которая «в теории невозможна», но если она всё же случилась — это точно баг, а не штатный сценарий.
func mustCompile(pattern string) *Matcher {
m, err := compile(pattern)
if err != nil {
panic("некорректный паттерн при инициализации: " + err.Error())
}
return m
}Обрати внимание на префикс must в имени функции — это распространённое в Go соглашение: если функция называется mustЧтоТо, то она паникует вместо возврата ошибки, и вызывающий код должен об этом знать заранее.
Как recover перехватывает панику внутри defer
Функция recover устроена не совсем обычно: она работает только будучи вызванной непосредственно внутри функции, отложенной через defer. Если вызвать recover в любом другом месте — например, просто где-то в середине обычного кода — она ничего не поймает и вернёт nil, даже если где-то рядом происходит паника.
Механика такая: когда случается panic, Go начинает «сматывать» стек вызовов вверх, по пути выполняя все отложенные через defer функции текущей горутины (в порядке LIFO — об этом подробно в следующем уроке). Если внутри одной из этих отложенных функций вызвать recover(), паника «гасится» прямо там: она перестаёт распространяться дальше вверх, а функция, в которой случилась паника, штатно (хоть и досрочно) завершается — выполнение возвращается к тому месту, откуда эту функцию вызывали, как будто ничего страшного не произошло.
func riskovannayaOperaciya() {
defer fmt.Println("Это выполнится в любом случае")
defer func() {
if r := recover(); r != nil {
fmt.Println("Поймали:", r)
}
}()
panic("что-то пошло не так")
}
func main() {
riskovannayaOperaciya()
fmt.Println("main продолжает работу как ни в чём не бывало")
}Вывод:
Поймали: что-то пошло не так
Это выполнится в любом случае
main продолжает работу как ни в чём не бывалоЗдесь важно, что оба defer всё равно выполнились — паника не «перепрыгивает» через отложенные вызовы, а честно проходит по каждому из них, просто в конце концов один из них её перехватил.
Частые ошибки на собеседовании
Первая ошибка — пытаться поймать панику без defer, просто вызвав recover() где-то в обычном потоке кода: такой вызов всегда вернёт nil и панику не остановит. Вторая, очень частая ошибка — злоупотреблять panic/recover как заменой обычной обработки ошибок, превращая их в подобие try/catch из других языков на каждый чих; в Go это считается плохим стилем, потому что усложняет чтение кода и скрывает нормальный путь ошибок через error. Третья ошибка — забыть, что recover перехватывает панику только в пределах своей горутины: если паника случилась в отдельно запущенной через go горутине, а recover стоит только в main, это не спасёт — паника в горутине без собственного перехвата уронит всю программу целиком.
Итоги
panic— это для настоящих программистских багов и неустранимых на месте ситуаций, а не замена обычной обработки ошибок черезerror.- Многие рантайм-ошибки в Go (выход за границы слайса, nil-указатель, деление на ноль) вызывают
panicавтоматически. recover()работает только будучи вызванной напрямую внутри функции, отложенной черезdefer— в любом другом месте она бесполезна.- Каждая горутина обрабатывает свою панику сама —
recoverв одной горутине не спасёт от паники в другой.