panic и recover
В прошлом уроке defer был списком дел у двери, который читают на выходе. Теперь представьте пожарную тревогу. Люди покидают здание этаж за этажом, но на каждом этаже по пути выполняют пункты списка у двери. Тревогу может отменить только тот, у кого в списке есть пункт «проверить и отменить тревогу».
Паника
В Go такая тревога называется паникой. Если программа во время работы делит целое число на ноль, начинается паника. Паника прерывает текущую функцию и все функции, которые её вызвали, но отложенные вызовы по пути выполняются:
package main
import "fmt"
func divide(a, b int) int {
return a / b
}
func main() {
defer fmt.Println("deferred call")
fmt.Println(divide(6, 3)) // 2
fmt.Println(divide(1, 0)) // ✗
}panic: runtime error: integer divide by zeroДавайте разберём, что выводит программа:
divide(6, 3)возвращает2, и программа печатает2.divide(1, 0)делит на ноль, и начинается паника.- Программа печатает
deferred call. - Только после этого появляется сообщение о панике.
Паника поднимается из divide в main, выполняет отложенные вызовы main и завершает программу. Такой подъём по цепочке вызовов называют разматыванием стека.
Откуда берётся паника
Паника начинается двумя путями. Значение, с которым она начинается, бывает любым.
Ошибка времени выполнения
Go сам начинает панику, когда операцию нельзя выполнить. Деление целого числа на ноль — такой случай. Значение этой паники — ошибка времени выполнения, и сообщение начинается со слов runtime error:, как в примере выше.
Вызов panic
Панику можно вызвать и самому — встроенной функцией panic. В скобках передают значение паники, например строку. После panic("no config") программа останавливается с сообщением panic: no config.
Функция recover
Встроенная функция recover останавливает панику. Она подчиняется трём правилам:
- работает только внутри отложенной функции;
- во время паники возвращает значение, переданное в
panic, и разматывание прекращается; - без паники возвращает
nil.
Отложенная функция выполняется во время разматывания, поэтому только в ней recover застаёт панику. Это и есть пункт «отменить тревогу» в списке у двери. Если отложенный вызов остановил панику, его функция сразу завершается, а вызвавший её код продолжает работу.
func safeDivide(a, b int) (q int, ok bool) {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
return a / b, true
}
func main() {
fmt.Println(safeDivide(6, 3)) // 2 true
fmt.Println(safeDivide(1, 0)) // 0 false
}Оба результата вызова можно передать в fmt.Println прямо, без промежуточных переменных: он печатает их через пробел. Давайте разберём оба вызова:
safeDivide(6, 3)делит без паники,recoverвозвращаетnil, и программа печатает2 true.safeDivide(1, 0)делит на ноль, и отложенная функция печатаетrecovered: runtime error: integer divide by zero.- Затем программа печатает
0 false. Инструкцияreturn a / b, trueдо конца не дошла, поэтому именованные результаты сохранили нулевые значения.
Здесь картинка с тревогой заканчивается. После отменённой тревоги люди возвращаются на свой этаж и продолжают работу. А safeDivide к прерванной строке не возвращается: она сразу завершается, и main получает её результаты как от обычного возврата и продолжает работу со следующей строки.
Ошибка вместо паники
Отложенная функция может не только напечатать панику, но и поменять результат. Именованные результаты видны ей по имени, а записанное в них значение функция и вернёт:
func safeDivide(a, b int) (q int, err error) {
defer func() {
if r := recover(); r != nil {
err = errors.New("division failed")
}
}()
return a / b, nil
}
func main() {
fmt.Println(safeDivide(6, 3)) // 2 <nil>
fmt.Println(safeDivide(1, 0)) // 0 division failed
}Давайте разберём второй вызов:
- Деление на ноль вызывает панику, и
return a / b, nilдо конца не доходит. - Отложенная функция ловит панику и записывает ошибку в
err. safeDivideвозвращаетqиerrкак есть:0иdivision failed.
Так паника внутри функции превращается для вызывающего кода в обычную ошибку.
Где работает recover
Этот вопрос часто задают на собеседовании. Ответ состоит из трёх частей:
recoverработает только в отложенной функции;- вызывать его нужно прямо в теле этой функции;
- в любом другом месте
recoverвозвращаетnilи панику не останавливает.
Паника или error
Сбои бывают двух видов, и для каждого свой инструмент:
- паника — для ошибок, которых не бывает в правильной программе;
- значение
error— для ожидаемого сбоя, например неверного ввода: функция его возвращает.
Частая ошибка
recover вызывают прямо в теле функции, а не в отложенной функции:
func safeDivide(a, b int) int {
recover() // ✗
return a / b
}Вызов safeDivide(1, 0) завершает программу той же паникой integer divide by zero:
recoverвыполняется в начале тела, когда паники ещё нет, и возвращаетnil.- Паника на следующей строке уже никем не останавливается.
Вызывайте recover внутри defer func() { … }(), как в safeDivide выше.
Главное
- Паника прерывает функцию и поднимается по цепочке вызовов, выполняя по пути отложенные вызовы.
recoverостанавливает панику, только если его вызвали в отложенной функции; без паники он возвращаетnil.- Функция, в которой остановили панику, сразу завершается, а вызвавший её код работает дальше.
- Отложенная функция может записать ошибку в именованный результат, и вызывающий код получит её вместо паники.
- Ожидаемые сбои возвращают через
error, а паника — для того, чего в правильной программе не бывает.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.