Введение

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

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

Знакомство с паниками

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

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

Паники при выходе за границы

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

В результате мы получаем следующий вывод:

Output
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. Необходимо помнить, что срезы и массивы ведут отчет с нуля, т. е. первый элемент будет иметь индекс ноль, а последний элемент этого среза — индекс 2. Мы пытаемся получить доступ к элементу среза с индексом 3, однако в срезе нет такого элемента, потому что он находится за пределами среза. У среды для выполнения нет других вариантов, кроме как прекращать работу и осуществлять выход, поскольку мы попросили сделать что-то невозможное. Также Go не может проверить во время компиляции, что этот код будет пытаться сделать это, поэтому с помощью компилятора нельзя поймать эту ошибку.

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

Структура паники

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

Первая часть любой паники — это сообщение. Оно всегда будет начинаться со строки 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]:. Заголовок предоставляет номер идентификатора гоурутины и состояние, в котором она находилась при возникновении паники. После заголовка трассировка стека показывает функцию, выполняемую программой во время генерации паники, а также имя файла и номер строки, где исполняется функция.

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

Ссылочные типы со значением nil

Язык программирования 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()
}

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

Output
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. Вместо получения дружественного сообщения от Sammy, мы получим панику, потому что мы пытались получить доступ к недействительному адресу в памяти. Поскольку переменная s равна nil, когда функция SayHello вызывается, она пытается получить доступ к полю Name типа *Shark. Поскольку это указатель получателя, а получатель в данном случае nil, генерируется паника, поскольку он не может разыменовывать указатель nil.

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

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

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

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

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

package main

func main() {
    foo()
}

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

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

Output
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!​​​ и трассировка стека отображает одну гоурутину с двумя строками в трассировке стека: одна для функции 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!")
}

Вывод, получаемый из этого примера, будет выглядеть следующим образом:

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

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

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

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

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

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
}

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

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

Наша функция main в данном примере вызывает функцию, которую мы определили, divideByZero. В этой функции мы используем defer для вызова анонимной функции, которая отвечает за работу с любыми паниками, которые могут возникнуть при исполнении 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
}

Результат будет выглядеть так:

Output
we survived dividing by zero!

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

Заключение

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

Также вы можете ознакомиться с нашей серией статей о программировании на языке Go.

0 Comments

Creative Commons License