Menu

defer в Golang: порядок, вычисление аргументов и ловушки

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

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Что делает defer

defer помещает вызов функции в список. Когда окружающая функция возвращает управление, список выполняется в обратном порядке.

Вывод:

start
end
deferred 3
deferred 2
deferred 1

Порядок «последним пришёл, первым ушёл», как у стека. Это соответствует вложенности ресурсов: если открыть A, а затем B, обычно B нужно закрыть раньше A.

Очистка рядом с захватом

Главное применение defer: записать очистку на строке сразу после захвата ресурса, чтобы ни один путь возврата о ней не забыл.

Обратите внимание на порядок: сначала проверка ошибки, потом defer. Если os.Open завершился ошибкой, f равен nil, и f.Close(), отложенный до проверки, вызвал бы Close на nil *os.File (это вернёт ошибку, которую вы никогда не увидите, и запутает читателя).

Для блокировок та же схема:

mu.Lock()
defer mu.Unlock()

Если код между ними запаникует, мьютекс всё равно будет разблокирован.

Аргументы вычисляются сразу

Отложенная функция и её аргументы вычисляются, когда выполняется инструкция defer. Ждёт только сам вызов.

Вывод:

x is now 2
deferred closure reads: 2
deferred with argument: 1

fmt.Println("...", x) запомнил значение 1 на строке с defer. У замыкания нет аргументов; оно читает x, когда наконец выполняется. Выбирайте форму в зависимости от того, что нужно зафиксировать.

Это касается и получателей методов. defer t.Stop() вычисляет t сразу, поэтому последующее переприсваивание t не меняет того, какое значение будет остановлено.

Популярный приём для замера времени использует это правило намеренно:

func handle() {
	defer trace("handle")() // trace runs now, the returned func runs at exit
	// ...
}

trace("handle") вызывается сразу (может напечатать «enter» и запомнить время начала), а откладывается функция, которую она возвращает.

defer в цикле

Отложенные вызовы выполняются, когда функция возвращает управление, а не в конце каждой итерации цикла. В цикле по множеству файлов это держит все файлы открытыми до конца функции.

for _, path := range paths {
	f, err := os.Open(path)
	if err != nil {
		return err
	}
	defer f.Close() // all files stay open until the function returns
	process(f)
}

При тысячах путей закончатся файловые дескрипторы. Вынесите тело в отдельную функцию, чтобы каждый defer срабатывал на каждой итерации:

Каждый вызов processFile закрывает свой файл до того, как откроется следующий. Функциональный литерал, вызванный на месте (func() { ... }()), работает так же, если отдельная именованная функция кажется излишней.

Изменение возвращаемых значений

Отложенное замыкание выполняется после того, как инструкция return присвоила результаты, и может изменить именованные результаты до того, как их увидит вызывающий код.

Программа печатает 10 и save failed: disk full. С неименованным результатом отложенная функция всё равно выполнится, но изменить возвращаемое значение не сможет.

Сохранение ошибки из Close

defer f.Close() выбрасывает ошибку из Close. Для файлов, которые вы только читаете, это нормально. Для файлов, в которые вы писали, Close может сообщить о неудачном финальном сбросе данных, так что ошибка важна. Именованный результат позволяет её сохранить:

func writeReport(path string, data []byte) (err error) {
	f, err := os.Create(path)
	if err != nil {
		return err
	}
	defer func() {
		if cerr := f.Close(); cerr != nil && err == nil {
			err = cerr
		}
	}()
	_, err = f.Write(data)
	return err
}

errors.Join(err, f.Close()) это более короткая альтернатива, если нужно сообщить об обеих ошибках.

defer, panic и recover

Отложенные вызовы выполняются, пока паника раскручивает стек. Это единственное место, где recover что-то делает, и так сервер не даёт одному плохому запросу убить весь процесс. Подробности на странице panic и recover.

Отложенные вызовы не выполняются, когда программа завершается через os.Exit или log.Fatal. Если main откладывает очистку, а затем вызывает os.Exit(1), очистка пропускается.

Стоимость

Начиная с Go 1.14 большинство defer компилятор встраивает прямо в код (open-coded), и они стоят несколько наносекунд. Использовать defer для каждого освобождения мьютекса и закрытия файла это нормальный стиль. Исключение составляют defer внутри циклов: их нельзя встроить, и они идут по более медленному пути, что ещё один повод выносить тела циклов в функции.

Частые ошибки

  • defer до проверки ошибки. Сначала проверьте err от Open, затем defer Close.
  • Ожидание очистки на каждой итерации цикла. defer выполняются при выходе из функции.
  • Ожидание, что отложенный аргумент увидит последующие изменения. Аргументы фиксируются на строке с defer. Чтобы прочитать значение при выходе, используйте замыкание.
  • Расчёт на defer вместе с os.Exit. Он не выполнится никогда.

Часто задаваемые вопросы

Что делает defer в Go?

defer f() откладывает вызов f() до момента, когда окружающая функция вернёт управление, неважно, нормально, через ранний return или из-за паники. Его используют, чтобы поставить очистку (закрытие файла, разблокировку мьютекса) рядом с кодом, который захватил ресурс.

В каком порядке выполняются отложенные вызовы в Go?

Последним пришёл, первым ушёл. Последний отложенный вызов выполняется первым. defer fmt.Println(1); defer fmt.Println(2) печатает 2, а затем 1.

Когда вычисляются аргументы отложенной функции?

Сразу, когда выполняется инструкция defer, а не когда происходит вызов. x := 1; defer fmt.Println(x); x = 2 печатает 1. Чтобы прочитать значение в момент выхода, отложите замыкание: defer func() { fmt.Println(x) }().

Выполняется ли defer при панике или os.Exit?

Отложенные вызовы выполняются, пока паника раскручивает стек, поэтому внутри них и работает recover. Они не выполняются, когда программа вызывает os.Exit (или log.Fatal, который его вызывает), и не выполняются в других горутинах, когда main возвращает управление.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ