Когда я начинал изучать Go, одна конструкция показалась мне необычной.
Это оператор defer. Несмотря на кажущуюся простоту, он скрывает достаточно нюансов, чтобы удивить даже опытного
разработчика.
Что такое defer и зачем он нужен#
defer позволяет отложить вызов функции до выхода из текущей функции. Неважно, как мы выходим: через return,
по завершении тела или при панике. Отложенная функция всё равно будет вызвана. Удобно для очистки ресурсов.
Где пригодится defer:
- Очистка ресурсов (закрытие файлов, соединений с БД, HTTP response body)
- Синхронизация (разблокировка мьютексов, ожидание WaitGroup)
- Обработка ошибок и паник
- Принудительная запись данных из буфера
- Измерение времени выполнения функций для метрик
Три правила defer#
- Аргументы отложенной функции вычисляются в момент выполнения оператора
defer, а не при фактическом вызове.
| |
Если нужно, чтобы defer увидел актуальное значение, используем замыкание. Оно захватывает переменную по ссылке:
| |
- Отложенные функции вызываются в порядке LIFO (Last In First Out).
| |
- Отложенные функции могут читать и изменять именованные возвращаемые значения функции.
| |
Как это работает: return "old" сначала присваивает value = "old", затем выполняются defer-функции (которые могут изменить value), и только потом функция реально возвращает значение.
defer, panic и recover#
Перехватить панику в Go можно только через defer. Когда функция вызывает panic, нормальное выполнение останавливается. Go начинает раскрутку стека: поднимается по вызовам вверх и на каждом уровне выполняет все defer-функции в LIFO-порядке. Если ни одна из них не вызвала recover, программа завершается с выводом стектрейса.
| |
Важная деталь: recover работает только при прямом вызове из defer-функции. Кажется логичным вынести обработку в хелпер, но это не сработает:
| |
Ещё одна тонкость: паника распространяется только по стеку текущей горутины. Если горутина паникует без recover, падает вся программа, а не только она.
| |
Ловушки и подводные камни#
Игнорирование ошибки#
| |
Выглядит правильно, но есть подвох. Если f.Close() вернёт ошибку, она потеряется. Чтобы её не потерять, делаем возвращаемую ошибку именованной и оборачиваем в defer. Благодаря третьему правилу defer-функция может прочитать err и даже изменить её.
| |
Альтернатива: использовать f.Sync() перед return для записи, а defer f.Close() оставить только для закрытия дескриптора.
| |
errors.Join ловит все ошибки, но требует именованных возвращаемых значений. f.Sync() проще: не нужны ни замыкания,
ни именованные значения. Разделение обязанностей: Sync() отвечает за данные, Close() за дескриптор.
defer внутри цикла#
defer работает на уровне функции, не блока. В цикле это создаёт проблему:
| |
defer откладывает вызов f.Close() до выхода из функции concatFiles. Значит, открытые дескрипторы накапливаются, и
все файлы закроются только в самом конце. Передадим слайс из 1000 filenames, и одновременно откроется 1000
дескрипторов. А ОС ограничивает их количество на процесс, обычно 1024 по умолчанию. На тысячу файлов может просто не хватить.
Как это исправить:
- Вынести чтение файла в отдельную функцию
- Не использовать
defer, вызыватьf.Close()явно - Использовать анонимную функцию с немедленным вызовом (IIFE)
Вот вариант с отдельной функцией:
| |
Value receiver в defer#
Первое правило говорит: аргументы вычисляются в момент defer. Но receiver метода тоже аргумент, хотя по виду и не скажешь. Если метод принимает value receiver, struct копируется при регистрации defer:
| |
Исправляем pointer receiver’ом:
| |
os.Exit()#
Отложенная функция не вызывается, если программа завершается с помощью os.Exit(). Это может ударить по graceful
shutdown. Если в main() настроены defer-функции для закрытия соединений или записи данных из буфера, а завершение идёт
через os.Exit() (или log.Fatal, который вызывает os.Exit(1) под капотом), то ничего из этого не выполнится.
| |
Заключение#
defer из тех механизмов Go, что просты на поверхности, но требуют понимания нюансов. Запомните три вещи: аргументы вычисляются при регистрации, а не при вызове; в циклах defer копит ресурсы до выхода из функции; ошибку Close() нельзя игнорировать при записи. Тогда defer станет надёжным инструментом, а не источником сюрпризов.