defer: порядок и аргументы

О лекции

defer ставит вызов в стек и выполняет его при выходе из функции, с конца. Аргументы считаются сразу, в момент defer, а не в момент выхода.

Рантайм и инструменты Go golang собеседование

defer нужен, чтобы не забыть Unlock, Close, cancel контекста, откат транзакции. Он привязан к функции, не к блоку. Несколько return и паника всё равно дойдут до отложенных вызовов этой функции. Это не finally произвольного куска кода и не деструктор объекта.

Порядок нескольких defer — с конца: последний зарегистрированный выполнится первым. Это LIFO. Так закрывают вложенные ресурсы в обратном порядке открытия. Аргументы функции, которую отложили, считаются сразу.

Аналогия: на дверь выхода клеите стикеры «не забудь». Выходите — читаете стикеры сверху вниз: последний приклеенный первым. На стикере Println(x) уже написано текущее число. Стикер с замыканием смотрит на переменную в момент выхода.

Аргументы, порядок, именованный return

func f() {
	x := 1
	defer fmt.Println(x) // напечатает 1, не 2
	x = 2
}

func g() (n int) {
	defer func() { n++ }()
	return 10 // вернёт 11: сначала n=10, потом defer
}

func loop() {
	for i := 0; i < 3; i++ {
		defer fmt.Println(i) // аргумент i копируется: 0,1,2; выход → 2 1 0
	}
}

Аргументы фиксируются при defer. Замыкание на именованный результат может его изменить.

Пример. defer fmt.Println(x) запомнил 1. Потом x стал 2 — печать всё равно 1. А defer func() { fmt.Println(x) }() прочитает x при выходе и напечатает 2.

Именованный возврат плюс defer — способ поправить ошибку или счётчик на выходе. Это же источник путаницы: читатель не видит, что return 10 не финальный. В длинной функции defer, который меняет err, легко пропустить.

Цикл и стоимость

defer в цикле на миллион итераций в одной функции копит миллион записей до return. Это память и задержка на выходе. Закрывать ресурс лучше в маленькой функции на итерацию: там defer дешёвый относительно I/O. Или закрывать явно, если профиль показал проблему. Современный компилятор подешевел defer в простых случаях. «Миллион defer в одной функции» всё ещё плохой вкус.

Пример. В func process(files []string) на каждый файл сделали defer f.Close(). Функция держит все файлы открытыми до конца. Лучше вынести обработку одного файла в helper с своим defer.

Паника в функции выполняет defer, затем идёт вверх по стеку горутины. recover только в defer этой же функции. Отложенный вызов сам может паникнуть — тогда предыдущий recover уже не поможет.

Типичная ошибка

Ждать, что аргументы defer пересчитаются на выходе: Println(x) запоминает x сейчас. В цикле замыкание на i без параметра на старых версиях печатает одно значение. Defer в огромном цикле одной функции копит вызовы, а не выполняется сразу каждую итерацию.

На собеседовании

Вызовы идут с конца (LIFO). Аргументы считаются при регистрации, замыкание — при выполнении. Именованный return defer может изменить. В цикле — маленький helper с defer, иначе вызовы копятся до выхода из функции.