Управление с помощью функций defer, panic и recover

Завершено

Теперь рассмотрим некоторые потоки управления, которые используются исключительно в языке Go: defer, panic и recover. У каждой из этих функций есть несколько вариантов использования. В этом разделе мы изучим самые важные варианты использования.

Функция Defer

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

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

Узнайте, как работает этот шаблон, выполнив следующий пример кода:

package main

import "fmt"

func main() {
    for i := 1; i <= 4; i++ {
        defer fmt.Println("deferred", -i)
        fmt.Println("regular", i)
    }
}

Ниже приведены выходные данные кода:

regular 1
regular 2
regular 3
regular 4
deferred -4
deferred -3
deferred -2
deferred -1

В этом примере обратите внимание, что каждый раз, когда fmt.Println("deferred", -i) он был отложен, значение для i хранения и вызов функции был добавлен в очередь. А после завершения вывода функцией main() значений regular запустились все отложенные вызовы. Обратите внимание, что выходные данные отложенных вызовов выполняются в обратном порядке (последнее в первом выходе), так как они выводятся из очереди.

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

package main

import (
    "io"
    "os"
    "fmt"
)

func main() {
    newfile, error := os.Create("learnGo.txt")
    if error != nil {
        fmt.Println("Error: Could not create file.")
        return
    }
    defer newfile.Close()

    if _, error = io.WriteString(newfile, "Learning Go!"); error != nil {
	    fmt.Println("Error: Could not write to file.")
        return
    }

    newfile.Sync()
}

После создания или открытия файла вы откладываете функцию .Close(), чтобы не забыть закрыть файл по окончании работы с ним.

Функция Panic

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

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

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

Например, следующий код объединяет функции panic и defer. Попробуйте выполнить этот код, чтобы увидеть, как прерывается поток управления. Обратите внимание, что процессы очистки по-прежнему выполняются.

package main

import "fmt"

func highlow(high int, low int) {
    if high < low {
        fmt.Println("Panic!")
        panic("highlow() low greater than high")
    }
    defer fmt.Println("Deferred: highlow(", high, ",", low, ")")
    fmt.Println("Call: highlow(", high, ",", low, ")")

    highlow(high, low + 1)
}

func main() {
    highlow(2, 0)
    fmt.Println("Program finished successfully!")
}

Появятся следующие выходные данные.

Call: highlow( 2 , 0 )
Call: highlow( 2 , 1 )
Call: highlow( 2 , 2 )
Panic!
Deferred: highlow( 2 , 2 )
Deferred: highlow( 2 , 1 )
Deferred: highlow( 2 , 0 )
panic: highlow() low greater than high

goroutine 1 [running]:
main.highlow(0x2, 0x3)
	/tmp/sandbox/prog.go:13 +0x34c
main.highlow(0x2, 0x2)
	/tmp/sandbox/prog.go:18 +0x298
main.highlow(0x2, 0x1)
	/tmp/sandbox/prog.go:18 +0x298
main.highlow(0x2, 0x0)
	/tmp/sandbox/prog.go:18 +0x298
main.main()
	/tmp/sandbox/prog.go:6 +0x37

Program exited: status 2.

Вот что происходит при выполнении кода:

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

  2. Если значение low больше значения high, программа переходит в режим тревоги. Отобразится сообщение Panic!. В этот момент поток управления прерывается, и все отложенные функции начинают выводить сообщение Deferred....

  3. Программа аварийно завершается, после чего отображается полная трассировка стека. Сообщение Program finished successfully! не отобразится.

Вызов функции panic() обычно выполняется, если серьезных ошибок не ожидается. Чтобы избежать сбоя программы, можно использовать другую функцию с именем recover().

Функция Recover

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

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

Попробуйте изменить функцию main в предыдущем коде, чтобы добавить вызов в функцию recover() следующим образом:

func main() {
    defer func() {
	handler := recover()
        if handler != nil {
            fmt.Println("main(): recover", handler)
        }
    }()

    highlow(2, 0)
    fmt.Println("Program finished successfully!")
}

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

Call: highlow( 2 , 0 )
Call: highlow( 2 , 1 )
Call: highlow( 2 , 2 )
Panic!
Deferred: highlow( 2 , 2 )
Deferred: highlow( 2 , 1 )
Deferred: highlow( 2 , 0 )
main(): recover from panic highlow() low greater than high

Program exited.

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

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

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

Дополнительные сведения см. в предложении по добавлению встроенной функции try в Go.