Теперь вы точно знаете, что во время выполнения программы могут возникать ошибки.

  • Иногда ошибки не несут большой проблемы — к примеру, пользователь передал неправильные данные. В таком случае достаточно сделать запись в логах.

  • Иногда ошибки могут создать проблемы, когда не удаётся получить информацию от внешних ресурсов. В таком случае можно уведомить пользователя или попробовать получить информацию ещё раз.

  • Иногда ошибки могут быть такими, что дальнейшее продолжение программы нецелесообразно и даже опасно. Например, не удалось подключиться к базе данных, возникли ошибки в конфигурационных файлах.

    Последние ситуации считаются аварийными и требуют немедленного завершения работы программы. Это делается с помощью механизма паники. С ним вы уже знакомы: программа вызывает панику, например, при разыменовании nil-указателя или неуспешном приведении типа. В этой главе рассмотрим панику детальнее.

В аварийной ситуации программа останавливает работу, вызываются отложенные функции defer и выводится сообщение об ошибке. В этом сообщении, кроме текста, указывается состояние стека вызовов функций.

Чтобы создать аварийную ситуацию, нужно вызвать встроенную функцию panic. В аргументе при вызове panic можно передать значение любого типа:

import (
    "fmt"
    "os"
)
 
func myfunc() {
    if _, err := os.ReadFile(`test.txt`); err != nil {
        panic(err)
    }
}
 
func main() {
    fmt.Println("Старт")
    myfunc()
    fmt.Println("Финиш")
}

Если запустить этот код, программа завершит работу с такой ошибкой и стеком:

Старт
panic: open test.txt: no such file or directory

goroutine 1 [running]:
main.myfunc(...)
    /tmp/sandbox910916036/prog.go:10
main.main()
    /tmp/sandbox910916036/prog.go:16 +0x11e 

Программа не дошла до печати строки "Финиш". Сразу можно увидеть не только причину ошибки, но и номер строки, где эта ошибка произошла.

Кроме функции panic, аварийные ситуации может генерировать среда выполнения Go по таким сценариям, как деление на ноль, разыменование нулевого указателя, обращение по индексу за границами массива или слайса. Подобные ошибки могут возникнуть в любой функции. Например, нерационально проверять выход за границы массива или равенство указателя nil при каждой операции:

import "fmt"
 
func mypanic() {
    var slice []string
    fmt.Println(slice[0])
}
 
func main() {
    mypanic()
}

Если запустить этот фрагмент, программа завершит работу с таким текстом ошибки:

panic: runtime error: index out of range [0] with length 0

goroutine 1 [running]:
main.mypanic()
    /tmp/sandbox852111250/prog.go:7 +0x18
main.main()
    /tmp/sandbox852111250/prog.go:11 +0x25 

Паника возникла в строке номер 7, в функции, которая была вызвана в строке 11.

Note

Не рекомендуется использовать функцию panic для обработки всех ошибок. Стоит избегать panic в библиотеках. Если такая функция всё-таки необходима, нужно обязательно это задокументировать.

pаnic — ресурсозатратный механизм, который не всегда можно остановить. Именно поэтому паника не рекомендуется к применению.

Вот пример описания для функции MustCompile стандартного пакета regexp:

// MustCompile is like Compile but panics if the expression cannot be parsed. It simplifies safe initialization of global variables holding compiled regular expressions.
func MustCompile(str string) *Regexp 

Создание аварийной ситуации возможно, когда непонятно, как программа будет выполняться дальше и лучше завершить процесс сейчас, чем получить более серьёзные проблемы. Например, паника уместна в тестах и сюжетах, когда программа на старте не может прочитать файл конфигурации. Ещё panic часто встречается в конструкциях switch — case, если значение переменной не соответствует ни одному варианту.

switch srvType {
case LocalHost:
    // ...
case RemoteHost:
    // ...
default:
    panic(fmt.Sprintf(`Unknown type of the server %d`, srvType))
} 

Рекомендуется логировать каждый случай паники и подключать систему оповещений разработчиков для быстрого информирования о проблеме.

Чем аварийная ситуация отличается от штатного завершения программы? В Go есть ещё одна функция — recover, которая позволяет восстановить выполнение программы в случае паники. Если на момент вызова recover произошла аварийная ситуация, то recover завершает её и возвращает значение ошибки (аргумент при вызове panic). Если аварийной ситуации не было, recover ничего не делает и возвращает nil.

Note

Не передавайте nil при вызове panic — если recover вернёт его, вы не поймёте, что была аварийная ситуация.

Где вызвать функцию recover, если программа остановила своё выполнение? Так как при возникновении аварийной ситуации вызываются функции defer, вставим вызов recover туда.

Напомним, что defer-вызовы будут вызваны в любом случае, даже если произошла паника. Мы рассматривали это подробно в разделе, посвящённом defer:

import "fmt"
 
func mypanic() {
    defer func() {
        if p := recover(); p != nil {
            fmt.Println(`Возникла паника: `, p)
        }
    }()
    panic(`aварийная ситуация`)
}
 
func main() {
    fmt.Println("Старт")
    mypanic()
    // функция main продолжит работу, так как использовали recover
    fmt.Println("Финиш")
} 
// Cтарт
// Возникла паника: aварийная ситуация
// Финиш 

Связка функций panic и recover напоминает механизм исключений try — catch в других языках. Но если стандартная работа с ошибками не требует дополнительных ресурсов, то вызов panic приводит к раскручиванию стека, а это затратная операция.

Несмотря на то что функция recover позволяет продолжить выполнение программы, её следует применять лишь в оправданных ситуациях. Например, стандартный веб-сервер использует recover, чтобы паника в одном обработчике не завершила весь процесс.

Также стоит помнить о возможных утечках памяти и неопределённых значений глобальных переменных после выхода из аварийной ситуации, что может привести к новым конфликтам. Существуют ситуации, когда паника не может быть отловлена, и программа однозначно завершается. Это, например, ситуации, связанные с конкурентностью или тем, что на компьютере закончилась память.

Ключевые мысли

  1. Паника прерывает нормальное исполнение программы, поэтому применима только в аварийных ситуациях.
  2. Паника вызывает непредвиденные расходы по ресурсам, связанные с раскруткой стека.
  3. Отложенные вызовы defer вызываются всегда. Именно там стоит ловить панику функцией recover.
  4. Не всякую панику можно восстановить.
  5. Даже если в пакете приходится вызывать панику, то её не следует выпускать за пределы пакета.

📂 Go | Последнее изменение: 30.08.2024 20:37