Перейти к содержимому
Теория · Функции как значения · урок 7 из 12

panic и recover

В прошлом уроке defer был списком дел у двери, который читают на выходе. Теперь представьте пожарную тревогу. Люди покидают здание этаж за этажом, но на каждом этаже по пути выполняют пункты списка у двери. Тревогу может отменить только тот, у кого в списке есть пункт «проверить и отменить тревогу».

Паника

В Go такая тревога называется паникой. Если программа во время работы делит целое число на ноль, начинается паника. Паника прерывает текущую функцию и все функции, которые её вызвали, но отложенные вызовы по пути выполняются:

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)) // ✗
}
TEXT
panic: runtime error: integer divide by zero

Давайте разберём, что выводит программа:

  1. divide(6, 3) возвращает 2, и программа печатает 2.
  2. divide(1, 0) делит на ноль, и начинается паника.
  3. Программа печатает deferred call.
  4. Только после этого появляется сообщение о панике.

Паника поднимается из divide в main, выполняет отложенные вызовы main и завершает программу. Такой подъём по цепочке вызовов называют разматыванием стека.

Откуда берётся паника

Паника начинается двумя путями. Значение, с которым она начинается, бывает любым.

Ошибка времени выполнения

Go сам начинает панику, когда операцию нельзя выполнить. Деление целого числа на ноль — такой случай. Значение этой паники — ошибка времени выполнения, и сообщение начинается со слов runtime error:, как в примере выше.

Вызов panic

Панику можно вызвать и самому — встроенной функцией panic. В скобках передают значение паники, например строку. После panic("no config") программа останавливается с сообщением panic: no config.

Функция recover

Встроенная функция recover останавливает панику. Она подчиняется трём правилам:

  • работает только внутри отложенной функции;
  • во время паники возвращает значение, переданное в panic, и разматывание прекращается;
  • без паники возвращает nil.

Отложенная функция выполняется во время разматывания, поэтому только в ней recover застаёт панику. Это и есть пункт «отменить тревогу» в списке у двери. Если отложенный вызов остановил панику, его функция сразу завершается, а вызвавший её код продолжает работу.

GO
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 прямо, без промежуточных переменных: он печатает их через пробел. Давайте разберём оба вызова:

  1. safeDivide(6, 3) делит без паники, recover возвращает nil, и программа печатает 2 true.
  2. safeDivide(1, 0) делит на ноль, и отложенная функция печатает recovered: runtime error: integer divide by zero.
  3. Затем программа печатает 0 false. Инструкция return a / b, true до конца не дошла, поэтому именованные результаты сохранили нулевые значения.

Здесь картинка с тревогой заканчивается. После отменённой тревоги люди возвращаются на свой этаж и продолжают работу. А safeDivide к прерванной строке не возвращается: она сразу завершается, и main получает её результаты как от обычного возврата и продолжает работу со следующей строки.

Ошибка вместо паники

Отложенная функция может не только напечатать панику, но и поменять результат. Именованные результаты видны ей по имени, а записанное в них значение функция и вернёт:

GO
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
}

Давайте разберём второй вызов:

  1. Деление на ноль вызывает панику, и return a / b, nil до конца не доходит.
  2. Отложенная функция ловит панику и записывает ошибку в err.
  3. safeDivide возвращает q и err как есть: 0 и division failed.

Так паника внутри функции превращается для вызывающего кода в обычную ошибку.

Где работает recover

Этот вопрос часто задают на собеседовании. Ответ состоит из трёх частей:

  • recover работает только в отложенной функции;
  • вызывать его нужно прямо в теле этой функции;
  • в любом другом месте recover возвращает nil и панику не останавливает.

Паника или error

Сбои бывают двух видов, и для каждого свой инструмент:

  • паника — для ошибок, которых не бывает в правильной программе;
  • значение error — для ожидаемого сбоя, например неверного ввода: функция его возвращает.

Частая ошибка

recover вызывают прямо в теле функции, а не в отложенной функции:

GO
func safeDivide(a, b int) int {
    recover() // ✗
    return a / b
}

Вызов safeDivide(1, 0) завершает программу той же паникой integer divide by zero:

  1. recover выполняется в начале тела, когда паники ещё нет, и возвращает nil.
  2. Паника на следующей строке уже никем не останавливается.

Вызывайте recover внутри defer func() { … }(), как в safeDivide выше.

Главное

  • Паника прерывает функцию и поднимается по цепочке вызовов, выполняя по пути отложенные вызовы.
  • recover останавливает панику, только если его вызвали в отложенной функции; без паники он возвращает nil.
  • Функция, в которой остановили панику, сразу завершается, а вызвавший её код работает дальше.
  • Отложенная функция может записать ошибку в именованный результат, и вызывающий код получит её вместо паники.
  • Ожидаемые сбои возвращают через error, а паника — для того, чего в правильной программе не бывает.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом