Обработка Паники в Го

Вступление

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

Паники попадают во вторую категорию ошибок, которые непредвидены программистом. Эти непредвиденные ошибки приводят к самопроизвольному завершению программы и выходу из запущенной программы Go. Распространенные ошибки часто ответственны за создание паники. В этом уроке мы рассмотрим несколько способов, которыми обычные операции могут вызывать панику в Go, и мы также увидим способы избежать этой паники. Мы также будем использовать https://www.digitalocean.com/community/tutorials/understanding-defer-in-go [+ defer +] операторы вместе с функцией + recovery + для захвата паники, прежде чем они смогут неожиданно завершить наши запущенные программы Go.

Понимание Паники

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

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

Паника вне пределов

Когда вы пытаетесь получить доступ к индексу, превышающему длину фрагмента или емкость массива, среда выполнения 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)])
}

Это будет иметь следующий результат:

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

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

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

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

Анатомия Паники

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

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

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

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

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

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

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

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

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

Ноль приемники

Язык программирования Go имеет указатели для ссылки на конкретный экземпляр некоторого типа, существующий в памяти компьютера во время выполнения. Указатели могут принимать значение + nil +, указывающее, что они ни на что не указывают. Когда мы пытаемся вызвать методы для указателя + 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()
}

Созданная паника будет выглядеть так:

Outputpanic: 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 +.

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

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

Использование встроенной функции + panic

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

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

package main

func main() {
   foo()
}

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

Выход паники выглядит следующим образом:

Outputpanic: oh no!

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

Здесь мы определяем функцию + foo +, которая вызывает встроенную функцию + panic + со строкой " о, нет! ". Эта функция вызывается нашей функцией + main +. Обратите внимание, что вывод имеет сообщение + panic: oh no! +, А трассировка стека показывает одну процедуру с двумя строками в трассировке стека: одну для функции + 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!")
}

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

Outputhello 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 этого примера мы сначала` + defer` вызываем анонимную функцию, которая печатает сообщение " hello от отложенной функции! ". Функция + main + немедленно вызывает панику с помощью функции + panic +. В выходных данных этой программы мы сначала видим, что отложенная функция выполняется и печатает ее сообщение. После этого возникает паника, которую мы создали в + main +.

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

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

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

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

package main

import (
   "fmt"
   "log"
)

func main() {
   divideByZero()
   fmt.Println("we survived dividing by zero!")

}

func divideByZero() {
   defer func() {
       if err := ; err != nil {
           log.Println("panic occurred:", err)
       }
   }()
   fmt.Println(divide(1, 0))
}

func divide(a, b int) int {
   return a / b
}

Этот пример выведет:

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

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

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

В выходных данных этого примера мы сначала видим сообщение журнала от анонимной функции, которая восстанавливает панику, затем следует сообщение «+ мы выжили, деля на ноль! +». Мы действительно сделали это, благодаря встроенной функции +cover +, которая остановила бы катастрофическую панику, которая привела бы к завершению нашей программы Go.

Значение + err +, возвращаемое из + recovery () +, является точно значением, которое было предоставлено для вызова + panic () +. Поэтому важно убедиться, что значение + err + равно нулю только тогда, когда паники не произошло.

Обнаружение паники с помощью + recovery +

Функция + recovery + полагается на значение ошибки, чтобы определить, возникла ли паника или нет. Поскольку аргумент функции + 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
}

Это выведет:

Outputwe survived dividing by zero!

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

Заключение

Мы видели несколько способов, которыми + panic + можно создавать в Go, и как их можно восстановить с помощью встроенной команды + recovery +. Хотя вы не обязательно используете + panic + сами, правильное восстановление после паники является важным шагом для подготовки приложений Go к работе.

Вы также можете изучить our всю статью «Как кодировать в Go».