Перейти к содержимому
Теория · Функции как значения · урок 5 из 12

defer

Представьте листок у входной двери, куда по ходу дня вы записываете дела перед уходом. Включили плиту — дописали «выключить плиту». Открыли окно — дописали «закрыть окно». Уходя, вы читаете список снизу вверх: сначала закрываете то, что открыли последним.

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

Отложенный вызов

Слово defer перед вызовом откладывает его до выхода из функции:

GO
package main

import "fmt"

func check(n int) string {
    defer fmt.Println("checked", n) // checked -1
    if n < 0 {
        return "negative"
    }
    return "ok"
}

func main() {
    fmt.Println(check(-1)) // negative
}

Вызов после defer называют отложенным вызовом — это пункт в списке у двери. Отложить можно вызов любой функции, в том числе своей: defer cleanup().

Давайте разберём пример по шагам:

  1. defer fmt.Println("checked", n) записывает печать в список, но пока ничего не печатает.
  2. Для -1 условие n < 0 верно, и check выходит через return "negative".
  3. В этот момент выполняется отложенный вызов и печатает checked -1.
  4. Только потом main получает результат "negative" и печатает его.

Когда выполняется вызов

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

  • через любой return;
  • после последней строки тела.

Отложенный вызов ждёт выхода из всей функции, а не конца итерации или блока.

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

Порядок LIFO

Если отложенных вызовов несколько, они выполняются в обратном порядке: последний отложенный — первым. Этот порядок называют LIFO (last in, first out — «последним пришёл, первым ушёл»):

GO
for i := range 3 {
    defer fmt.Print(i) // 210
}

Разберём, что здесь происходит:

  1. Цикл откладывает три вызова — с 0, 1 и 2.
  2. При выходе из main первым выполняется последний из них, с 2.
  3. За ним идут вызовы с 1 и 0, и программа печатает 210.

Частая ошибка

Отложенный вызов должен напечатать итог, а печатает начальное значение:

GO
total := 0
defer fmt.Println("total:", total) // ✗
total += 5

Программа печатает total: 0. Go вычисляет аргументы отложенного вызова сразу, на строке с defer, а откладывает только сам вызов. По этому же правилу в цикле выше каждый отложенный вызов запомнил своё значение i.

Здесь картинка со списком заканчивается. Настоящий листок вы перечитываете при уходе и видите, что творится в доме сейчас. А Go хранит пункт вместе с уже готовыми аргументами: «напечатать total: 0», и позже их никто не переписывает.

Чтобы прочитать переменную при выходе, отложите вызов замыкания:

GO
total := 0
defer func() { fmt.Println("total:", total) }() // total: 5
total += 5

Разберём строку с defer:

  • скобки после литерала вызывают его: defer принимает только вызов;
  • аргументов у этого вызова нет, поэтому сразу вычислять нечего;
  • замыкание читает total в момент выполнения, когда там уже 5.

Два правила defer

Этот вопрос часто задают на собеседовании: в каком порядке выполнятся отложенные вызовы и какие значения они напечатают? Ответ — те два правила, что мы разобрали:

  1. Порядок LIFO: последний отложенный вызов выполняется первым.
  2. Аргументы берутся с той строки, где стоит defer.

Главное

  • defer откладывает вызов до выхода из функции — через любой return или после конца тела.
  • Отложенные вызовы выполняются в порядке LIFO: последний отложенный — первым.
  • Аргументы отложенного вызова вычисляются сразу, на строке с defer.
  • Чтобы прочитать переменную при выходе, откладывают вызов замыкания: defer func() { … }().

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом