Глиф Учебники

defer в Go — что это и как работает

Справочник · Go · 2 минуты чтения

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

Go
package main

import "fmt"

func main() {
    fmt.Println("начало")
    defer fmt.Println("отложено 1")
    defer fmt.Println("отложено 2")
    fmt.Println("конец")
}
Вывод
начало
конец
отложено 2
отложено 1

Отложенные вызовы выполняются в обратном порядке: последний записанный — первым. Так удобно освобождать ресурсы: что открыли позже, закрывается раньше.

Зачем это нужно

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

Go
f, err := os.Open("data.txt")
if err != nil {
    return err
}
defer f.Close()
// … чтение файла; при любом выходе файл закроется

Так же пишут defer mu.Unlock() после mu.Lock(), defer wg.Done() в горутине, defer out.Flush() для буферизованного вывода. Это аналог блока finally из других языков.

Аргументы вычисляются сразу

Функция откладывается, но её аргументы вычисляются в момент, когда выполняется строка с defer:

Go
package main

import "fmt"

func main() {
    x := 1
    defer fmt.Println("defer видит", x)
    x = 2
    fmt.Println("x =", x)
}
Вывод
x = 2
defer видит 1

Если нужно последнее значение, откладывают функцию без аргументов, которая прочитает переменную сама: defer func() { fmt.Println(x) }().

recover: перехват паники

Внутри отложенной функции можно вызвать recover(): он останавливает панику и возвращает её значение. Вместе с именованным результатом это превращает панику в обычную ошибку:

Go
package main

import "fmt"

func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("перехвачено: %v", r)
        }
    }()
    return a / b, nil
}

func main() {
    fmt.Println(safeDivide(10, 2))
    fmt.Println(safeDivide(1, 0))
}
Вывод
5 <nil>
0 перехвачено: runtime error: integer divide by zero

(result int, err error) — именованные результаты: отложенная функция может изменить их уже после return. recover работает только внутри функции, отложенной через defer; в обычном коде он возвращает nil. Пользуйтесь им осторожно — обычные ошибки в Go возвращают через error, а не через панику.

Ловушки

defer в цикле. Отложенные вызовы выполнятся только в конце функции, а не в конце шага цикла:

Go
package main

import "fmt"

func main() {
    for i := range 3 {
        defer fmt.Println("defer", i)
    }
    fmt.Println("цикл закончен")
}
Вывод
цикл закончен
defer 2
defer 1
defer 0

Если в цикле открываются тысячи файлов с defer f.Close(), все они останутся открытыми до выхода из функции. Выносите тело цикла в отдельную функцию — тогда defer сработает на каждом шаге.

os.Exit и log.Fatal. os.Exit завершает программу сразу, и отложенные вызовы не выполняются. То же с log.Fatal, который вызывает os.Exit(1). Буфер bufio.Writer при этом не сбросится, а временные файлы не удалятся.

Ошибка из Close. defer f.Close() отбрасывает ошибку закрытия. Для чтения это не страшно, а при записи файла ошибку закрытия лучше проверить явно — иначе можно не узнать, что данные не записались на диск.