defer в Go
Открыл файл - тут же напиши defer f.Close(), и освобождение больше не потеряется ни на одном из семи путей выхода из функции. Механизм на вид тривиальный, но у него три особенности, на которых регулярно ломаются: порядок выполнения, момент вычисления аргументов и доступ к именованным результатам.
Стержень: отложенные вызовы выполняются в обратном порядке, аргументы вычисляются в момент объявления, а замыкание видит переменные в момент выполнения.
// Формулировки: «в каком порядке выполнятся defer?», «что напечатает defer, если переменную изменить после него?», «зачем defer в цикле - плохо?»
Порядок обратный, и это логично
Отложенные вызовы складываются в стек и выполняются в обратном порядке. Проверено на цикле из трёх итераций: печатается 2, 1, 0.
Порядок не прихоть: ресурсы обычно захватывают вложенно. Открыл соединение, начал транзакцию, взял блокировку - освобождать надо в обратной последовательности, иначе получится, что транзакция закрывается после соединения.
Отложенный вызов сработает при любом выходе из функции: обычный return, ранний return из проверки ошибки, паника. Именно поэтому его ставят сразу после успешного захвата ресурса, а не в конце функции.
// Тонкость: defer привязан к функции, а не к блоку. Отложенный вызов внутри if или for выполнится не по выходу из блока, а по выходу из всей функции - об этом следующая карточка.
Момент вычисления: аргументы против замыкания
Аргументы отложенного вызова вычисляются в момент объявления defer, а не в момент выполнения. Проверено: переменная равна 1, ставим defer с ней, потом меняем на 99 - печатается 1. Логика в том, что defer запоминает конкретный вызов с конкретными значениями.
Замыкание ведёт себя противоположно. defer func() { fmt.Println(x) }() печатает 99: тело выполняется в конце, и переменная к тому моменту уже другая. Разница - в том, что именно отложено: готовый вызов с вычисленными аргументами или функция, которая всё вычислит потом.
Из этой пары складываются обе типичные ошибки. Хотел замерить длительность, а записал время начала как аргумент. Или наоборот - рассчитывал на зафиксированное значение, а замыкание подставило последнее.
// Мнемоника простая: defer со скобками аргументов - снимок сейчас, defer с телом функции - взгляд потом.
x := 1
defer fmt.Println("аргумент:", x) // напечатает 1 - снимок в момент defer
defer func() { fmt.Println("замыкание:", x) }() // напечатает 99
x = 99Именованный результат и правильный Close
Отложенная функция видит именованные результаты и может их менять. Проверено: функция возвращает 1 и nil, а defer подменяет их на 42 и ошибку - наружу уходит именно подменённое.
Это единственный способ поймать ошибку закрытия. При записи в файл Close возвращает ошибку - именно там всплывают проблемы сброса буфера, и потерять её означает потерять данные молча. Приём: именованный результат err и defer, который подставляет ошибку закрытия, если основной ошибки не было.
На чтении Close обычно игнорируют осознанно, и это нормально: терять там нечего. Разница между «игнорирую, потому что подумал» и «игнорирую, потому что не знал» - один комментарий в коде.
// Другая сторона той же медали: не пиши в defer то, что молча меняет результат неочевидным образом. Отложенная функция, переписывающая ошибку без объяснения, - находка для того, кто будет отлаживать это через год.
func write(name string) (err error) {
f, err := os.Create(name)
if err != nil { return err }
defer func() {
if cerr := f.Close(); err == nil {
err = cerr // не теряем ошибку закрытия
}
}()
_, err = f.WriteString("данные")
return err
}Цикл и os.Exit
defer выполняется по выходу из функции, а не из итерации цикла. Значит defer f.Close() внутри цикла по десяти тысячам файлов означает десять тысяч открытых дескрипторов до конца функции - и почти наверняка ошибку «too many open files».
Лечение стандартное: тело итерации выносят в отдельную функцию, тогда defer срабатывает на каждой. Либо закрывают явно в конце итерации, без defer.
Второе исключение - os.Exit. Он завершает процесс немедленно, отложенные вызовы не выполняются вообще. То же самое с log.Fatal, который внутри вызывает os.Exit: файлы не закроются, буферы не сбросятся, временные каталоги останутся.
// Поэтому log.Fatal уместен разве что в main при старте. В библиотечном коде он превращает любую ошибку в мгновенную смерть процесса мимо всей логики завершения, которую кто-то старательно писал.
Как отвечать: «Что напечатает defer, если переменную изменить после него?»
Зависит от того, что именно отложено. Если это вызов с аргументом - defer fmt.Println(x) - аргумент вычисляется в момент объявления, то есть будет напечатано старое значение. Я проверял: x равен 1, ставим defer, меняем на 99, печатается 1. Если же отложено замыкание - defer func() { fmt.Println(x) }() - тело выполнится в конце функции и увидит 99. Мнемоника: со скобками аргументов это снимок сейчас, с телом функции - взгляд потом. Раз уж речь о defer, назову ещё три вещи, которые обычно спрашивают следом. Порядок обратный, стеком: три defer в цикле напечатают 2, 1, 0 - и это правильно, потому что ресурсы захватывают вложенно. Отложенная функция видит именованные результаты и может их подменить - на этом строится правильное закрытие файла на запись, где нельзя потерять ошибку Close. И defer привязан к функции, а не к блоку, поэтому defer внутри длинного цикла копит дескрипторы до конца функции; лечится выносом тела итерации в отдельную функцию.
Ответ разводит два случая с проверенными значениями, даёт мнемонику и добавляет три смежных факта, которые всё равно спросят следующими.
На чём валят
- −Ждут прямого порядка выполнения отложенных вызовов - он обратный.
- −Путают снимок аргумента и замыкание: одно печатает старое значение, другое новое.
- −Ставят defer Close внутри цикла и упираются в лимит открытых файлов.
- −Теряют ошибку Close при записи, потому что не используют именованный результат.
- −Рассчитывают на defer после os.Exit или log.Fatal - они не выполняются вовсе.
- −Ставят defer до проверки ошибки открытия и закрывают то, что не открылось.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 8, остальные разбираются в тренажёре.
- i := 1; defer fmt.Println(i); i = 99. Что напечатает этот defer при выходе?A)1 — аргументы defer вычисляются в момент объявленияB)99 — defer видит финальное значение переменной при выходе из функцииC)Оба значения по очереди: сперва 1, затем 99D)Ничего: defer с изменённой переменной молча пропускается
показать ответ и разбор
+A)1 — аргументы defer вычисляются в момент объявления// разбор: Аргументы отложенного вызова вычисляются В МОМЕНТ выполнения оператора defer, а не когда defer сработает. Поэтому fmt.Println(i) «замораживает» i=1, и последующее i=99 на вывод не влияет — напечатает 1. Нужно позднее значение — оборачивают в замыкание: defer func(){ fmt.Println(i) }(). Спот-чек: напечатало 1.
- func f() (n int) { defer func(){ n *= 2 }(); n = 5; return n }. Что вернёт f()?A)5 — оператор return зафиксировал значение ещё до выполнения deferB)10 — defer через замыкание правит именованный результат после returnC)0 — defer сбрасывает результат к нулевому значениюD)Ошибка компиляции: изменять именованный результат в defer запрещено
показать ответ и разбор
+B)10 — defer через замыкание правит именованный результат после return// разбор: return n у функции с ИМЕНОВАННЫМ результатом сначала присваивает n возвращаемое значение, затем выполняются defer'ы — и defer через замыкание видит и меняет n (n *= 2 → 10) уже после return, но до фактической отдачи вызывающему. Поэтому f() вернёт 10. Этим, например, дополняют возвращаемую ошибку в defer. Спот-чек: 10.
- В цикле по тысяче файлов внутри одной функции пишем defer f.Close() после открытия. Что не так?A)Ничего: defer закрывает файл сразу в конце каждой итерации циклаB)Close вызовется дважды на файл — в defer и позже при сборке мусораC)Компилятор запретит оператор defer внутри тела циклаD)Все Close отложатся до конца ФУНКЦИИ — тысяча дескрипторов будет висеть
показать ответ и разбор
+D)Все Close отложатся до конца ФУНКЦИИ — тысяча дескрипторов будет висеть// разбор: defer привязан к выходу из ФУНКЦИИ, а не из итерации цикла. Тысяча defer f.Close() накопится и выполнится разом в конце функции — всё это время дескрипторы открыты, можно упереться в лимит ОС (too many open files). Лечение: вынести тело в отдельную функцию (тогда defer сработает на каждой итерации) или закрывать явно в конце итерации, без defer.
- Выполнится ли отложенный вызов, если функция завершается паникой?A)Нет, если паника произошла до того, как выполнилась строка с deferB)Нет: паника прерывает функцию немедленно, минуя отложенные вызовыC)Да, но только если в этой же функции вызван recoverD)Да: defer срабатывает и при обычном возврате, и при раскрутке из-за паники
показать ответ и разбор
+D)Да: defer срабатывает и при обычном возврате, и при раскрутке из-за паники// разбор: Паника раскручивает стек, вызывая отложенные функции каждого кадра, — на этом и держится recover, который иначе негде было бы вызвать. Именно поэтому освобождение ресурсов пишут через defer: файл закроется и при обычном возврате, и при аварии. Не выполнится defer только в одном случае: если до строки с ним управление не дошло.
- Отменяет ли os.Exit(1) выполнение отложенных вызовов?A)Нет: перед завершением рантайм выполнит все зарегистрированные deferB)Да: процесс завершается сразу, отложенные вызовы не выполняютсяC)Нет: defer выполнятся, но только в главной горутине программыD)Да, но буферы записи будут сброшены на диск автоматически
показать ответ и разбор
+B)Да: процесс завершается сразу, отложенные вызовы не выполняются// разбор: os.Exit обрывает процесс немедленно: ни defer, ни сброс буферов не происходят. Отсюда типичная потеря данных, когда файл писали через буферизованный writer и вышли по ошибке — содержимое не доехало на диск. Идиома простая: из main возвращаются нормально, а завершение с кодом делают в тонкой обёртке, где никаких defer уже нет.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.