defer был придуман в Go для упрощения управления ресурсами (например, файлами, mutex, соединениями) — для любого случая, когда нужно гарантировать выполнение операций в самом конце выполнения функции. Исторически аналогичные конструкции были в других языках (finally в Java, try-with-resources) но Go реализует более явный и понятный паттерн.
Проблема: Нужно всегда быть уверенным, что ресурсы освобождаются, даже если выходит ошибка или происходит panic. Двойной вызов закрытия ресурса или утечка — частая проблема в классическом стиле программирования.
Решение: Всё, что объявлено через defer в функции или методе, помещается в стек вызова и будет выполнено в обратном порядке перед выходом из функции. Это гарантирует освобождение ресурсов даже при исключениях (panic) или преждевременных return.
Пример кода:
func processFile() error { f, err := os.Open("filename.txt") if err != nil { return err } defer f.Close() // закрытие файла случится в конце // работа с файлом return nil }
Ключевые особенности:
Выполнятся ли defer'ы, если внутри функции произошёл panic?
Да! Все defer-функции будут вызваны даже в случае panic, это основной механизм "финализации".
Когда вычисляются аргументы функции, переданные в 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 }() }
Плюсы:
Минусы: