[Перевод] Часть 32: паникуй и выздоравливай на голанге

Go
  • Оригинальный адрес:Part 32: Panic and Recover
  • Оригинальный автор:Naveen R
  • Переводчик: хаки хаки Пожалуйста, укажите источник.

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

Идиоматический способ обрабатывать исключения в Go - это использовать ошибки, которые достаточны для большинства исключений, которые возникают в программе.

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

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

Паника и восстановление чем-то похожи на операторы try-catch-finally в других языках, но первый используется реже, а его код более элегантный и лаконичный при использовании.

Когда следует использовать панику?

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

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

  2. CODER люди ошибаются Предположим, у нас есть метод, принимающий указатель в качестве параметра, но использующий NIL в качестве параметра для вызова этого метода. В этом случае мы можем использовать PANIC, потому что метод требует допустимого указателя.

пример паники

Определение функции паники

func panic(interface{})

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

package main

import (
    "fmt"
)

func fullName(firstName *string, lastName *string) {
    if firstName == nil {
        panic("runtime error: first name cannot be nil")
    }
    if lastName == nil {
        panic("runtime error: last name cannot be nil")
    }
    fmt.Printf("%s %s\n", *firstName, *lastName)
    fmt.Println("returned normally from fullName")
}

func main() {
    firstName := "Elon"
    fullName(&firstName, nil)
    fmt.Println("returned normally from main")
}

Run in playground

В приведенном выше коде функция fullName состоит в том, чтобы напечатать полное имя человека. Эта функция проверяет, равны ли нулю указатели firstName и lastName. Если он равен нулю, функция вызывает panic и отображает соответствующее сообщение об ошибке. Это сообщение об ошибке и информация о стеке ошибок выводятся при завершении программы.

Запуск этой программы напечатает следующий вывод:

panic: runtime error: last name cannot be nil
goroutine 1 [running]:
main.fullName(0x1040c128, 0x0)
    /tmp/sandbox135038844/main.go:12 +0x120
main.main()
    /tmp/sandbox135038844/main.go:20 +0x80

Давайте проанализируем этот вывод, чтобы понять, как работает паника и как печатаются трассировки стека. В строке 19 мы определяем Элона как firstName. Затем вызовите функцию fullName, где параметр lastName равен нулю. Итак, строка 11 вызовет панику. Когда срабатывает паника, выполнение программы прекращается, затем печатается то, что было передано панике, и, наконец, распечатывается трассировка стека. Так что код после 14 строк выполняться не будет. Сначала программа печатает то, что было передано функции паники,

panic: runtime error: last name cannot be nil

Затем распечатайте трассировку стека. Программа вызывает панику в строке 12, поэтому

ain.fullName(0x1040c128, 0x0)
    /tmp/sandbox135038844/main.go:12 +0x120

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

main.main()
    /tmp/sandbox135038844/main.go:20 +0x80

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

функция отсрочки

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

В приведенном выше примере у нас нет вызовов функций отсрочки. Измените приведенный выше пример, чтобы увидеть пример функции отсрочки.

package main

import (
    "fmt"
)

func fullName(firstName *string, lastName *string) {
    defer fmt.Println("deferred call in fullName")
    if firstName == nil {
        panic("runtime error: first name cannot be nil")
    }
    if lastName == nil {
        panic("runtime error: last name cannot be nil")
    }
    fmt.Printf("%s %s\n", *firstName, *lastName)
    fmt.Println("returned normally from fullName")
}

func main() {
    defer fmt.Println("deferred call in main")
    firstName := "Elon"
    fullName(&firstName, nil)
    fmt.Println("returned normally from main")
}

Run in playgroundЕдинственное изменение, внесенное в предыдущий код, — это добавление вызовов функций отсрочки в первой строке функции fullName и основной функции. результат пробега,

deferred call in fullName
deferred call in main
panic: runtime error: last name cannot be nil

goroutine 1 [running]:
main.fullName(0x1042bf90, 0x0)
    /tmp/sandbox060731990/main.go:13 +0x280
main.main()
    /tmp/sandbox060731990/main.go:22 +0xc0

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

В нашем случае defer объявлен в первой строке функции fullName. Сначала выполните функцию fullName. Распечатать

deferred call in fullName

Затем вызовите отсрочку, которая возвращает основную функцию,

deferred call in main

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

функция восстановления

recovery — это встроенная функция горутины для восстановления после прерванной паники. Функция определяется следующим образом:

func recover() interface{}

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

Измените его и используйте recovery, чтобы восстановить нормальное выполнение panic.

package main

import (
    "fmt"
)

func recoverName() {
    if r := recover(); r!= nil {
        fmt.Println("recovered from ", r)
    }
}

func fullName(firstName *string, lastName *string) {
    defer recoverName()
    if firstName == nil {
        panic("runtime error: first name cannot be nil")
    }
    if lastName == nil {
        panic("runtime error: last name cannot be nil")
    }
    fmt.Printf("%s %s\n", *firstName, *lastName)
    fmt.Println("returned normally from fullName")
}

func main() {
    defer fmt.Println("deferred call in main")
    firstName := "Elon"
    fullName(&firstName, nil)
    fmt.Println("returned normally from main")
}

Run in playgroundСтрока 7 вызывает функцию recoveryName. Значение, возвращаемое восстановлением, печатается здесь, Обнаружено, что функция recovery возвращает содержимое файла panic.

Он печатает следующим образом,

recovered from  runtime error: last name cannot be nil
returned normally from main
deferred call in main

Программа вызывает панику в строке 19, а функция defer recoveryName восстанавливает контроль над горутиной, вызывая recovery.

recovered from  runtime error: last name cannot be nil

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

returned normally from main
deferred call in main

Паника, восстановление и горутины

recovery работает только при вызове из той же горутины. Невозможно оправиться от паники, вызванной различными горутинами. Еще один пример для углубления понимания.

package main

import (
    "fmt"
    "time"
)

func recovery() {
    if r := recover(); r != nil {
        fmt.Println("recovered:", r)
    }
}

func a() {
    defer recovery()
    fmt.Println("Inside A")
    go b()
    time.Sleep(1 * time.Second)
}

func b() {
    fmt.Println("Inside B")
    panic("oh! B panicked")
}

func main() {
    a()
    fmt.Println("normally returned from main")
}

Run in playgroundВ приведенной выше программе функция b вызывает панику в строке 23. Функция a вызывает восстановление функции отсрочки для восстановления после паники. Строка 17 функции a использует другую горутину для выполнения функции b. Роль Sleep заключается только в том, чтобы гарантировать, что программа не будет завершена до завершения работы b.Конечно, эту проблему также можно решить с помощью sync.WaitGroup.

Как вы думаете, что выводит этот код? Справится ли паника? Ответ - нет. Паника не пройдет. Это связано с тем, что восстановление существует в другой горутине, а запуск паники происходит в функции b, выполняемой другой горутиной. Поэтому его нельзя восстановить. результат пробега,

Inside A
Inside B
panic: oh! B panicked

goroutine 5 [running]:
main.b()
    /tmp/sandbox388039916/main.go:23 +0x80
created by main.a
    /tmp/sandbox388039916/main.go:17 +0xc0

Из вывода видно, что восстановление не удалось.

Если функция b вызывается в той же горутине, паника возобновляется. В строке 17 поставить,go b()заменитьb()тогда он будет выводиться,

Inside A
Inside B
recovered: oh! B panicked
normally returned from main

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

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

type Error interface {
    error
    // RuntimeError is a no-op function but
    // serves to distinguish types that are run time
    // errors from ordinary errors: a type is a
    // run time error if it has a RuntimeError method.
    RuntimeError()
}

Интерфейс runtime.Error соответствует встроенному типу интерфейсаerror.

Давайте напишем надуманный пример для создания паники во время выполнения.

package main

import (
    "fmt"
)

func a() {
    n := []int{5, 7, 4}
    fmt.Println(n[3])
    fmt.Println("normally returned from a")
}
func main() {
    a()
    fmt.Println("normally returned from main")
}

Run in playgroundВ приведенной выше программе в строке 9 мы пытаемся получить доступ к n[3], что является недопустимым индексом в срезе. Это вызовет панику, вывод будет следующим:

panic: runtime error: index out of range

goroutine 1 [running]:
main.a()
    /tmp/sandbox780439659/main.go:9 +0x40
main.main()
    /tmp/sandbox780439659/main.go:13 +0x20

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

package main

import (
    "fmt"
)

func r() {
    if r := recover(); r != nil {
        fmt.Println("Recovered", r)
    }
}

func a() {
    defer r()
    n := []int{5, 7, 4}
    fmt.Println(n[3])
    fmt.Println("normally returned from a")
}

func main() {
    a()
    fmt.Println("normally returned from main")
}

Run in playgroundвывод после выполнения,

Recovered runtime error: index out of range
normally returned from main

Судя по всему видно, что паника восстановилась.

Получить информацию о стеке после восстановления

Мы возобновляем панику, но теряем информацию о вызове стека для этой паники. Один из способов решить эту проблему — использовать функцию PrintStack в пакете Debug для печати трассировки стека.

package main

import (
    "fmt"
    "runtime/debug"
)

func r() {
    if r := recover(); r != nil {
        fmt.Println("Recovered", r)
        debug.PrintStack()
    }
}

func a() {
    defer r()
    n := []int{5, 7, 4}
    fmt.Println(n[3])
    fmt.Println("normally returned from a")
}

func main() {
    a()
    fmt.Println("normally returned from main")
}

Run in playgroundВызовите debug.PrintStack в строке 11, вы можете увидеть последующий вывод,

Recovered runtime error: index out of range
goroutine 1 [running]:
runtime/debug.Stack(0x1042beb8, 0x2, 0x2, 0x1c)
    /usr/local/go/src/runtime/debug/stack.go:24 +0xc0
runtime/debug.PrintStack()
    /usr/local/go/src/runtime/debug/stack.go:16 +0x20
main.r()
    /tmp/sandbox949178097/main.go:11 +0xe0
panic(0xf0a80, 0x17cd50)
    /usr/local/go/src/runtime/panic.go:491 +0x2c0
main.a()
    /tmp/sandbox949178097/main.go:18 +0x80
main.main()
    /tmp/sandbox949178097/main.go:23 +0x20
normally returned from main

Как видно из вывода, сначала паника восстанавливается, а потом печатаетсяRecovered runtime error: index out of range, а затем распечатайте трассировку стека. Наконец распечатать после возобновления паникиnormally returned from main