Владислав Андреев - Блог

Defer, Panic и Recover в Go

Это перевод статьи Defer, Panic, and Recover из официального блога The Go Blog.

Go имеет обычные операторы для управления ходом программы: if, for, switch, goto. Также существует оператор go для запуска кода в отдельной горутине (goroutine). Я бы хотел обсудить некоторые из менее обычных: defer, panic и recover.

Defer

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

Например, давайте посмотрим на функцию, которая открывает два файла и копирует содержимое одного файла в другой:

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }

    written, err = io.Copy(dst, src)
    dst.Close()
    src.Close()
    return
}

Это сработает, но есть один баг. Если вызов os.Create не срабатывает, функция выполняется без закрытия файла. Это легко можно исправить, разместив вызов src.Close до второго return. Но если функции были бы сложнее, проблема могла быть не так легко найдена и решена. Путём введения оператора defer мы можем гарантировать, что файлы будут всегда закрыты.

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }
    defer src.Close()

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }
    defer dst.Close()

    return io.Copy(dst, src)
}

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

Поведение defer прямолинейно и предсказуемо. Вот три простых правила:

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

В этом примере выражение i рассчитывается, когда выполнился отложенный вызов Println. Этот вызов выведет 0 после выполнения функции.

func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
}

2. Вызов отложенной функции происходит в порядке Last In First Out после того, как выполнилась внешняя функция.

Эта функция выводит “3210”:

func b() {
    for i := 0; i < 4; i++ {
        defer fmt.Print(i)
    }
}

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

В этом примере отложенная функция инкрементирует возвращаемое значение i после того, как внешняя функция выполнилась. Таким образом, эта функция возвращает 2:

func c() (i int) {
    defer func() { i++ }()
    return 1
}

Panic - это встроенная функция, которая останавливает обычный режим работы программы и начинает панику. Когда функция F вызывает panic, работа функции прекращается, любые отложенные функции внутри F выполняются, как обычно, и затем F возвращает управление к вызвавшему её коду. Для вызывающего кода F ведёт себя так будто подаёт призыв к панике. Процесс продолжается вверх по стеку, пока все функции текущей горутины не закончат своё выполнение, после чего программа завершает свою работу. Можно инициировать панику, вызвав panic напрямую. Также паники (panics) могут быть связаны с ошибками времени выполнения, как, например, обращение к элементам массива за его границей.

Recover - это встроенная функция, которая восстанавливает контроль над паникующей горутиной. Recover полезен только внутри отложенных функций. Во время обычного выполнения вызов recover вернёт nil и ни на что не повлияет. Если текущая горутина паникует, вызов recover получит значение, переданное panic и вернётся к обычной работе.

Вот пример программы, которая демонстрирует, как работает panic и defer:

package main

import "fmt"

func main() {
    f()
    fmt.Println("Returned normally from f.")
}

func f() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()
    fmt.Println("Calling g.")
    g(0)
    fmt.Println("Returned normally from g.")
}

func g(i int) {
    if i > 3 {
        fmt.Println("Panicking!")
        panic(fmt.Sprintf("%v", i))
    }
    defer fmt.Println("Defer in g", i)
    fmt.Println("Printing in g", i)
    g(i + 1)
}

Функция g принимает int i и паникует, если i > 3 или если она вызывает сама себя с аргументом i+3. Функция f откладывает выполнение функции, которая вызывает recover и выводит сохранённое значение (если оно отличается от nil). Попробуйте подумать, каким будет вывод это программы, прежде чем продолжить чтение.

Вывод этой программы будет таким:


Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
Recovered in f 4
Returned normally from f.

Если мы удалим отложенную функция из f, то panic не будет покрыт recovered и достигнет вершины стека вызовов горутины (goroutine’s call stack), закончив выполнение программы. Обновлённая программа выведет:


Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
panic: 4
 
panic PC=0x2a9cd8
[трассировка стека опускается]

Рассмотрим пример использования panic/recover в реальном мире, взглянув на пакет json из стандартной библиотеки Go. Она декодирует JSON-данные во множество рекурсивных функций. Когда встречается неправильный JSON, обработчик вызывает panic для освобождения стека от вызова функции, которая восстанавливается из паники и возвращает соответствующее значение ошибки (см. методы ‘error’ и ‘unmarshal’ типа decodeState в decode.go)

Соглашение о библиотеках Go таково, что даже когда пакет паникует внутри, его внешний API всё равно даёт явные значения возвращаемых ошибок.

Другие случаи использования defer (помимо примера с file.Close, данного ранее) включают в себя освобождение mutex:

mu.Lock()
defer mu.Unlock()

вывод футера:

printHeader()
defer printFooter()

и т. д.

Итак, оператор defer (с и без panic/recover) предоставляет необычный и мощный механизм управления ходом работы программы. Он может использоваться для проектирования ряда фич, которые в других языках программирования реализованны при помощи специальных структур.