Пользователь
0,0
рейтинг
7 мая 2011 в 23:56

Разработка → Обработка ошибок в Go: Defer, Panic и Recover перевод

В языке 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. Замечания об опечатках и других неточностях просьба сообщать личными сообщениями, так как в силу разных причин смогу их исправлять только вечером.
Перевод: Andrew Gerrand
Бушу́ев Стас @Xitsa
карма
75,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (14)

  • 0
    Выкат C++ исключений и деструкторов вручную. Большой прогресс по сравнению с чистым Си, но всё же плюсовики будут кривиться.
  • 0
    Отложенное освобождение ресурсов хорошее. panic и recover как-то удивительно сделали, я не совсем понимаю преимущества относительно работающей во многих языках идиомы try / catch.
    • +3
      Роб Пайк недолюбливает эту идиому как раз из-за того, что наличие управляющей структуры try/catch/finally приводит к злоупотреблению исключениями и смешению с ошибками.
      Поэтому чтобы уменьшить это злоупотребление повышена гранулярность: с блока до функции.
      • 0
        Есть много споров на тему, нужно ли обязывать пользователя контролировать исключения try-catch блоком, или же делать это по усмотрению. Преимущество первых даже не в том, что обязует пользователя сразу писать безопасный код, а в том, что пользователь видит сразу все типы исключений, выбрасываемых всей цепочкой функций.
        Не знаю как для Пайка, но для меня try-catch-finally блок имеет гораздо более понятную и логичную структуру, чем defer: код, который идет ниже, выполняется всегда позже; тогда как выполнение defer скачет. Вообще в Go создает впечатление некоего компилируемого скриптового языка, нежели современногоязыка программирования.
  • 0
    Интерестный подход.

    Вопрос: в Go есть RAII? Т.е. defer сделан в дополнение к нему (как scope в D) или он компенсирует его отсутствие?
    • 0
      Go — язык со сборкой мусора. Поэтому деструкторов (а значит и RAII) в нем нет.
  • +3
    И да, мне одному кажется что «гопроцедура» неудачный термин для русского языка? Я постоянно читаю это слово не правильно.
    • 0
      Лучше поменять на «go-рутина». Так будет ближе к оригиналу, и понятнее.
      • НЛО прилетело и опубликовало эту надпись здесь
        • 0
          можно писать го-программа, так будет легче прочитать правильно.
      • 0
        У меня друг так говорит. Но это как-то не по-русски. Есть, кстати, очень похожий русский термин — волокно (поток, переключаемый приложением, у волокон в одном потоке общий TLS). Так что можно называть их go-волокнами.
  • +1
    Мне показалось или в примере после пункта 3 ошибка и функция должна возвращать I вместо 1? Иначе я вообще не понимаю что там происходит…
    • +1
      Я не совсем понял вопрос, потому отвечу в рамках своего разумения:
      у функции одно возвращаемое значение, оно именованное.
      Когда пишем return 1; компилятор догадывается, что мы возвращаем именованный возврат в i;
      И при выходе из функции значение с именем i равно 1;

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.