defer было введено в Go для упрощения управления ресурсами (например, файлами, мьютексами, соединениями) — в любых случаях, когда необходимо гарантировать выполнение операций в самом конце функции. Аналогичные конструкции существовали в других языках (finally в Java, try-with-resources), но Go реализует более явный и понятный паттерн.
Проблема: Нужно всегда быть уверенным, что ресурсы освобождаются, даже если произошла ошибка или паника. Двойной вызов закрытия ресурса или утечка — частая проблема в классическом программировании.
Решение: Всё, что объявлено через defer в функции или методе, помещается в стек вызова и будет выполнено в обратном порядке перед выходом из функции. Это гарантирует освобождение ресурсов даже при исключениях (паника) или преждевременных return.
Пример кода:
func processFile() error { f, err := os.Open("filename.txt") if err != nil { return err } defer f.Close() // закрытие файла произойдёт в конце // работа с файлом return nil }
Ключевые особенности:
Выполнятся ли defer'ы, если внутри функции произошла паника?
Да! Все defer-функции будут вызваны даже в случае паники, это основной механизм "финализации".
Когда вычисляются аргументы функции, переданные в defer?
В момент объявления defer, а не когда она реально исполняется. Поэтому, если использовать переменные, которые изменяются позже, это важно учитывать:
a := 1 defer fmt.Println(a) a = 2 // выведет 1, а не 2
Как работает defer внутри цикла? Не приведёт ли это к утечке памяти?
Если в каждой итерации цикла используется defer, то все defer'ы выполнятся только после завершения всей функции, а не после каждой итерации — весь стек defer-функций накопится, что может привести к избыточному потреблению памяти.
for i := 0; i < 3; i++ { defer fmt.Println(i) }
Открывается тысяча файлов в цикле, и для каждого используется defer. Все файлы будут закрыты только в конце всей функции, и ресурсы будут задержаны, что приведёт к "сливу" — превышению лимита открытых файлов.
Плюсы:
Минусы:
В цикле используются локальные функции, где defer применяют только для области этого файла, а не для всего обработчика:
for _, name := range fileNames { func() { f, _ := os.Open(name) defer f.Close() // работа с f }() }
Плюсы:
Минусы: