Обработка паники в Go

Ошибки, с которыми сталкивается программа, подразделяются на две широкие категории: предвиденные и не предвиденные программистом. Интерфейс error, который мы рассмотрели в предыдущих мануалах по обработке ошибок, в основном имеет дело с предвиденными ошибками программ на Go. Интерфейс error позволяет предусмотреть даже редкую возможность возникновения ошибки при вызовах функций, поэтому программа может адекватно реагировать в таких ситуациях.

Читайте также: Обработка ошибок в Go

Паники обрабатывают вторую категорию ошибок, то есть те, которые программист не предвидел. Эти непредвиденные ошибки приводят к самопроизвольному завершению работы программы Go. Распространенные ошибки часто создают паники. В этом мануале мы рассмотрим несколько способов, которыми обычные операции могут вызвать панику в Go, и научимся избегать паники. Мы будем использовать операторы defer вместе с функцией recover, чтобы зафиксировать панику, прежде чем у нее появится возможность неожиданно завершить работу программы Go.

Что такое паника?

В Go есть определенные операции, которые автоматически возвращают панику и останавливают программу. К этим операциям относится вызов индекса, который превышает границы массива, утверждение типа, вызов методов с нулевыми указателями, некорректное использование мьютексов и попытка работы с закрытыми каналами. Большинство из этих ситуаций являются результатом ошибок, допущенных при программировании, которые компилятор не может обнаружить во время компиляции программы.

Поскольку паника содержит подробности, полезные для решения проблемы, разработчики обычно используют панику как признак того, что они допустили ошибку во время разработки программы.

Паника вне границ

Когда вы пытаетесь получить доступ к индексу, который превышает длину фрагмента или емкость массива, среда выполнения Go вызывает панику.

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

package main
import (
"fmt"
)
func main() {
names := []string{
"lobster",
"sea urchin",
"sea cucumber",
}
fmt.Println("My favorite sea creature is:", names[len(names)])
}

Это вернет такой вывод:

panic: runtime error: index out of range [3] with length 3
goroutine 1 [running]:
main.main()
/tmp/sandbox879828148/prog.go:13 +0x20

Вывод паники содержит подсказку:

panic: runtime error: index out of range

Мы создали срез из трех элементов. Затем мы попытались получить последний элемент среза, проиндексировав этот срез по длине с помощью встроенной функции len. Помните, что срезы и массивы начинаются с нуля; поэтому первый элемент в этом срезе имеет индекс 0, а последний элемент – индекс 2. Поскольку команда обнаружила 3 элемента, она пытается получить доступ к элементу по индексу 3, но в срезе нет такого элемента – он находится за границами среза. Среда выполнения не может ничего сделать, только завершить работу и выйти, поскольку ей предложили сделать невозможное. Кроме того, Go не может обнаружить эту ошибку во время компиляции.

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

Компоненты паники

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

Первая часть любой паники – это сообщение. Оно всегда начинается со строки panic:, а далее следует строка, которая меняется в зависимости от причины паники. Паника из предыдущего примера имеет такое сообщение:

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

Строка «runtime error:» после префикса panic сообщает, что паника была сгенерирована средой выполнения языка. Эта паника говорит, что мы попытались использовать индекс [3], который был вне диапазона среза.

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

goroutine 1 [running]:
main.main()
/tmp/sandbox879828148/prog.go:13 +0x20

Эта трассировка из предыдущего примера показывает, что наша программа сгенерировала панику из файла /tmp/sandbox879828148/prog.go в строке под номером 13. Она также говорит, что паника была сгенерирована в функции main() из основного пакет.

Трассировка стека разбивается на отдельные блоки — по одному для каждой процедуры goroutine в программе. Обработка операций каждой программы Go выполняется одной или несколькими процедурами, каждая из которых может независимо и одновременно выполнять части вашего кода Go. Каждый блок начинается с заголовка goroutine X [state]:. В заголовке указан ID программы, а также состояние, в котором она находилась, когда возникла паника. После заголовка трассировка стека показывает функцию, которую программа выполняла, когда произошла паника, а также имя файла и номер строки, где выполнялась функция.

Паника в предыдущем примере была вызвана доступом к элементу вне границ среза. Паника также может быть сгенерирована при вызове методов для неустановленных указателей.

Нулевые указатели

Язык программирования Go поддерживает указатели для ссылки на конкретный экземпляр какого-либо типа, существующий в памяти компьютера во время выполнения. Указатели могут принимать значение nil, указывающее, что они ни на что не указывают. Когда мы пытаемся вызвать методы с нулевым указателем, среда выполнения Go сгенерирует панику. Точно так же переменные, которые являются типами интерфейса, будут вызывать панику при обращении к ним методов. Чтобы посмотреть на панику, возникающую в этих случаях, попробуйте запустить следующий пример:

package main
import (
"fmt"
)
type Shark struct {
Name string
}
func (s *Shark) SayHello() {
fmt.Println("Hi! My name is", s.Name)
}
func main() {
s := &Shark{"Sammy"}
s = nil
s.SayHello()
}

Паника вернет такой вывод:

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xdfeba]
goroutine 1 [running]:
main.(*Shark).SayHello(...)
/tmp/sandbox160713813/prog.go:12
main.main()
/tmp/sandbox160713813/prog.go:18 +0x1a

В этом примере мы определили структуру под названием Shark. У Shark есть один метод, определенный в приемнике указателя, который называется SayHello, он при вызове выводит приветствие на стандартный вывод. В теле функции main мы создаем новый экземпляр этой структуры Shark и запрашиваем указатель на нее с помощью оператора &. Этот указатель присваивается переменной s. Затем мы присваиваем переменной s значение nil с помощью оператора s = nil. После этого мы пытаемся вызвать метод SayHello для переменной s. Вместо ожидаемого сообщения мы получаем панику, так как мы попытались получить доступ к неверному адресу памяти. Поскольку переменная s равна nil, при вызове функции SayHello она пытается получить доступ к полю Name в типе *Shark. Это приемник указателя, а указатель в этом случае равен нулю, и программа паникует, потому что не может разыменовать нулевой указатель.

В этом примере мы явно присвоили переменной s значение nil, а на практике это происходит не так очевидно. Когда вы видите панику, связанную с nil pointer dereference, убедитесь, что вы правильно присвоили все переменные указателей, которые есть у вас в программе.

Паника, генерируемая нулевыми указателями и выходом за границы допустимого диапазона, — это две распространенные паники, генерируемые средой выполнения. Также панику можно сгенерировать вручную, используя встроенную функцию.

Функция panic

Также вы можете сгенерировать собственную панику, используя встроенную функцию panic. В качестве аргумента она принимает одну строку – сообщение, которое будет выводить паника. Обычно это сообщение короче, чем код для возврата ошибки. Кроме того, панику можно использовать это в собственных пакетах, чтобы указать разработчикам, что они могли ошибиться при использовании кода из вашего пакета. По возможности лучше возвращать значения error потребителям ваших пакетов.

Запустите этот код, чтобы увидеть панику, сгенерированную из функции, вызванной из другой функции:

package main
func main() {
foo()
}
func foo() {
panic("oh no!")
}

Вывод паники выглядит так:

panic: oh no!
goroutine 1 [running]:
main.foo(...)
/tmp/sandbox494710869/prog.go:8
main.main()
/tmp/sandbox494710869/prog.go:4 +0x40

Здесь мы определили функцию foo, которая вызывает встроенную функцию panic со строкой «oh no!». Эта функция вызывается функцией main. Обратите внимание, на выходе появляется сообщение panic: oh no!, а трассировка стека показывает одну процедуру goroutine с двумя строками (одна для функции main(), вторая для функции foo()).

Итак, паника останавливает программу там, где она появляется. Это может создать проблемы, если у вас есть открытые ресурсы, которые следует надлежащим образом закрыть. Go предоставляет механизм для выполнения отдельных фрагментов кода даже при возникновении паники.

Отложенные функции

Ваша программа может иметь ресурсы, которые нужно очистить, даже если во время выполнения обрабатывается паника. Go позволяет отложить выполнение вызова функции до тех пор, пока вызывающая функция не завершит работу. Отложенные функции работают даже при возникновении паники. По сути они используются в качестве защитного механизма от хаотического характера паник. Функции откладываются путем их обычного вызова, а затем для всего оператора ставится префикс с ключевым словом defer (например, defer sayHello()). Запустите этот пример, чтобы посмотреть, как программа выведет сообщение даже после возникновения паники:

package main
import "fmt"
func main() {
defer func() {
fmt.Println("hello from the deferred function!")
}()
panic("oh no!")
}

Результат в этом примере будет выглядеть так:

hello from the deferred function!
panic: oh no!
goroutine 1 [running]:
main.main()
/Users/gopherguides/learn/src/github.com/gopherguides/learn//handle-panics/src/main.go:10 +0x55

В этом примере в рамках функции main мы сначала откладываем вызов анонимной функции, которая выводит сообщение «hello from the deferred function!». Затем функция main немедленно производит панику, используя функцию panic. В выходных данных этой программы мы увидим, что отложенная функция выполняется и выводит свое сообщение.

Отложенные функции обеспечивают защиту от непредсказуемости, свойственной панике. Отложенные функции Go также позволяют предотвратить завершение программы Go в случае паники (с помощью другой встроенной функции).

Обработка паники

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

Поскольку функция recover является частью встроенного пакета builtin, recover можно вызвать без импорта каких-либо дополнительных пакетов:

package main
import (
"fmt"
"log"
)
func main() {
divideByZero()
fmt.Println("we survived dividing by zero!")
}
func divideByZero() {
defer func() {
if err := recover(); err != nil {
log.Println("panic occurred:", err)
}
}()
fmt.Println(divide(1, 0))
}
func divide(a, b int) int {
return a / b
}

Этот пример вернет:

2009/11/10 23:00:00 panic occurred: runtime error: integer divide by zero
we survived dividing by zero!

Функция main в этом примере вызывает функцию, которую мы определяем, divideByZero. В рамках этой функции мы откладываем вызов анонимной функции, отвечающей за работу со всеми паниками, которые могут возникнуть при выполнении divideByZero. В этой отложенной анонимной функции мы вызываем встроенную функцию recover и присваиваем переменной ошибку, которую она возвращает. Если divideByZero вызывает панику, это значение error будет установлено, в противном случае оно будет равно нулю (nil). Сравнивая переменную err с nil, мы можем определить, возникла ли паника. В таком случае мы регистрируем панику, используя функцию log.Println (как с любой другой ошибкой).

После этой отложенной анонимной функции мы вызываем другую функцию, которую мы определили, divide, и пытаемся отобразить ее результаты, используя fmt.Println. Предоставленные аргументы заставят divide делить на ноль, что вызовет панику.

В выходных данных этого примера мы сначала увидим сообщение лога от анонимной функции, которая восстанавливает панику. После него будет сообщение we survived dividing by zero!. Мы добились этого благодаря встроенной функции recover, которая остановила катастрофическую панику – иначе паника привела бы к завершению работы программы Go.

Значение err, возвращаемое функцией recover() – это то же значение, которое было предоставлено для вызова функции panic(). Поэтому очень важно, чтобы значение err было равно nil, если паника не возникла.

Обнаружение паники с помощью функции recover

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

package main
import (
"fmt"
"log"
)
func main() {
divideByZero()
fmt.Println("we survived dividing by zero!")
}
func divideByZero() {
defer func() {
if err := recover(); err != nil {
log.Println("panic occurred:", err)
}
}()
fmt.Println(divide(1, 0))
}
func divide(a, b int) int {
if b == 0 {
panic(nil)
}
return a / b
}

Этот код выведет:

we survived dividing by zero!

Этот пример почти такой же, как предыдущий, с некоторыми небольшими изменениями. Функция divide была изменена: теперь она должна проверить, равен ли ее делитель b нулю. Если это так, то она сгенерирует панику, используя встроенную функцию panic с аргументом nil. Выходные данные теперь не включают в себя сообщение лога, показывающее, что возникла паника (даже если она была создана функцией divide). Именно из-за этого замалчивания очень важно убедиться, что аргумент встроенной функции panic  не равен nil.

Заключение

Мы рассмотрели несколько путей возникновения паники в Go и узнали, как восстановить работу с помощью встроенной функции recover. Самостоятельно использовать функцию panic можно не всегда, но правильное восстановление после паники является важным шагом для подготовки приложений Go к работе.

Tags: ,