четверг, 13 февраля 2020 г.

Defer, Panic, и Recover в Golang

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

Утверждение 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 утверждения позволяют нам думать о закрытии каждого файла сразу после его открытия, гарантируя, что, независимо от количества утверждений возврата в функции, файлы будут закрыты.

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

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

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

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

2. Отложенные вызовы функций выполняются в порядке "последний пришел - первый вышел" (Last In First Out, LIFO) после возврата окружающей функции.

Эта функция печатает "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 останавливается, все отложенные (defer) функции в F выполняются нормально, а затем F возвращается к своему вызывающему. Для вызывающей стороны F ведет себя как вызов panic. Процесс продолжается вверх по стеку до тех пор, пока не будут возвращены все функции в текущей процедуре, после чего программа завершится сбоем. Panic можно инициировать, вызывая panic напрямую. Они также могут быть вызваны ошибками во время выполнения, такими как доступ за пределы массива.

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

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

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 + 1. Функция f откладывает функцию, которая вызывает recover и печатает восстановленное значение (если оно не равно нулю).

Программа выведет:

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.

Если мы удалим отложенную (defer) функцию из f, panic не восстановится и достигнет вершины стека вызовов программы, завершив программу. Эта измененная программа выведет:

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
[stack trace omitted]

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

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

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

mu.Lock()
defer mu.Unlock()

печать нижнего колонтитула:

printHeader()
defer printFooter()

и другие.

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


Читайте также:


Комментариев нет:

Отправить комментарий