Pull to refresh

Обработка ошибок в Go: Defer, Panic и Recover

Reading time 5 min
Views 61K
Original author: Andrew Gerrand
В языке Go используются обычные способы управления потоком выполнения: if, for, switch, goto. Есть ещё оператор go, чтобы запустить код в отдельной го-процедуре. А сейчас я бы хотел обсудить менее обычные способы: defer, panic и recover.

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

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


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

    dst, err := os.Open(dstName, os.O_WRONLY|os.O_CREATE, 0644)
    if err != nil {
        return
    }

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


Код рабочий, но в нём ошибка. Если второй вызов os.Open не удастся, функция завершит выполнение, оставив первый файл открытым. Это легко исправить, добавив вызов src.Close() перед вторым return'ом, но если функция посложнее, то эту проблему можно упустить. Введя команды defer, можно сделать, чтобы файлы закрылись при любых условиях:

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

    dst, err := os.Open(dstName, os.O_WRONLY|os.O_CREATE, 0644)
    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. Отложенные вызовы функций выполняются в порядке 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 останавливается, все отложенные вызовы в F выполняются нормально, затем F возвращает управление вызывающей функции. Для вызывающей функции вызов F ведёт себя как вызов panic. Процесс продолжается вверх по стеку, пока все функции в текущей го-процедуре не завершат выполнение, после чего аварийно останавливается программа. Паника может быть вызвана прямым вызовом panic, а также вследствие ошибок времени выполнения, таких как доступ вне границ массива.

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 принимает на вход целое 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.


Если мы уберем отложенный вызов функции из f, то паника не останавливается и достигает верха стека вызовов го-процедуры, останавливая программу. Так модифицированная программа выведет:

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» в decode.go). Похожий пример такой техники в процедуре Compile пакета regexp. Существует соглашение, что в библиотеках Go, даже если пакет использует panic внутри, его внешнее API возвращает явные коды ошибок.

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

mu.Lock()
defer mu.Unlock()

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

printHeader()
defer printFooter()


и много другое.

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

P.S. Замечания об опечатках и других неточностях просьба сообщать личными сообщениями, так как в силу разных причин смогу их исправлять только вечером.
Tags:
Hubs:
+27
Comments 14
Comments Comments 14

Articles