В языке 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. Замечания об опечатках и других неточностях просьба сообщать личными сообщениями, так как в силу разных причин смогу их исправлять только вечером.