defer в Go — что это и как работает
defer откладывает вызов функции до момента, когда закончится функция, в которой он написан. Отложенный вызов выполнится при любом выходе: после последней строки, на return и даже при панике — аварийной остановке программы.
package main
import "fmt"
func main() {
fmt.Println("начало")
defer fmt.Println("отложено 1")
defer fmt.Println("отложено 2")
fmt.Println("конец")
}начало
конец
отложено 2
отложено 1Отложенные вызовы выполняются в обратном порядке: последний записанный — первым. Так удобно освобождать ресурсы: что открыли позже, закрывается раньше.
Зачем это нужно
Главное применение — не забыть закрыть или освободить то, что взяли. defer пишут сразу после того, как ресурс получен, и дальше о нём можно не думать, сколько бы return ни было в функции:
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:
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(): он останавливает панику и возвращает её значение. Вместе с именованным результатом это превращает панику в обычную ошибку:
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 в цикле. Отложенные вызовы выполнятся только в конце функции, а не в конце шага цикла:
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() отбрасывает ошибку закрытия. Для чтения это не страшно, а при записи файла ошибку закрытия лучше проверить явно — иначе можно не узнать, что данные не записались на диск.