defer
Представьте листок у входной двери, куда по ходу дня вы записываете дела перед уходом. Включили плиту — дописали «выключить плиту». Открыли окно — дописали «закрыть окно». Уходя, вы читаете список снизу вверх: сначала закрываете то, что открыли последним.
Функции нужен такой же листок. Функция с несколькими return часто должна сделать одно и то же на любом выходе: например, записать строку в журнал или закрыть файл. Повторять эту строку перед каждым return неудобно, и новый return можно оставить без неё.
Отложенный вызов
Слово defer перед вызовом откладывает его до выхода из функции:
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().
Давайте разберём пример по шагам:
defer fmt.Println("checked", n)записывает печать в список, но пока ничего не печатает.- Для
-1условиеn < 0верно, иcheckвыходит черезreturn "negative". - В этот момент выполняется отложенный вызов и печатает
checked -1. - Только потом
mainполучает результат"negative"и печатает его.
Когда выполняется вызов
Отложенный вызов выполняется, когда функция завершается. Выйти она может двумя путями:
- через любой
return; - после последней строки тела.
Отложенный вызов ждёт выхода из всей функции, а не конца итерации или блока.
Обычно defer пишут сразу после действия, которое нужно завершить: открыли файл — следующей строкой откладывают его закрытие. Так начало и конец работы стоят рядом, и новый return их не разлучит.
Порядок LIFO
Если отложенных вызовов несколько, они выполняются в обратном порядке: последний отложенный — первым. Этот порядок называют LIFO (last in, first out — «последним пришёл, первым ушёл»):
for i := range 3 {
defer fmt.Print(i) // 210
}Разберём, что здесь происходит:
- Цикл откладывает три вызова — с
0,1и2. - При выходе из
mainпервым выполняется последний из них, с2. - За ним идут вызовы с
1и0, и программа печатает210.
Частая ошибка
Отложенный вызов должен напечатать итог, а печатает начальное значение:
total := 0
defer fmt.Println("total:", total) // ✗
total += 5Программа печатает total: 0. Go вычисляет аргументы отложенного вызова сразу, на строке с defer, а откладывает только сам вызов. По этому же правилу в цикле выше каждый отложенный вызов запомнил своё значение i.
Здесь картинка со списком заканчивается. Настоящий листок вы перечитываете при уходе и видите, что творится в доме сейчас. А Go хранит пункт вместе с уже готовыми аргументами: «напечатать total: 0», и позже их никто не переписывает.
Чтобы прочитать переменную при выходе, отложите вызов замыкания:
total := 0
defer func() { fmt.Println("total:", total) }() // total: 5
total += 5Разберём строку с defer:
- скобки после литерала вызывают его:
deferпринимает только вызов; - аргументов у этого вызова нет, поэтому сразу вычислять нечего;
- замыкание читает
totalв момент выполнения, когда там уже5.
Два правила defer
Этот вопрос часто задают на собеседовании: в каком порядке выполнятся отложенные вызовы и какие значения они напечатают? Ответ — те два правила, что мы разобрали:
- Порядок LIFO: последний отложенный вызов выполняется первым.
- Аргументы берутся с той строки, где стоит
defer.
Главное
deferоткладывает вызов до выхода из функции — через любойreturnили после конца тела.- Отложенные вызовы выполняются в порядке LIFO: последний отложенный — первым.
- Аргументы отложенного вызова вычисляются сразу, на строке с
defer. - Чтобы прочитать переменную при выходе, откладывают вызов замыкания:
defer func() { … }().
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.