JavaRush /Курсы /Go SELF /defer — момент вычисления аргументов и порядок LIFO

defer — момент вычисления аргументов и порядок LIFO

Go SELF
18 уровень , 0 лекция
Открыта

1. Зачем нужен defer

Если вы только начинаете, то идея «отложить вызов функции» звучит как что-то очень подозрительное. Типа «почему бы не написать cleanup() в конце и не жить спокойно?» Проблема в том, что «в конце» — это не одна точка. В реальном коде у вас часто несколько return: где-то проверили вход, где-то получили ошибку, где-то условие не подошло — и вы вышли раньше. defer решает именно это: он позволяет поставить «обязательное действие на выходе» рядом с тем местом, где оно становится обязательным.

Простейшая модель для головы такая: defer — это «напомни мне сделать вот это перед выходом из функции». Причём «перед выходом» означает перед выходом из текущей функции, не из main, не из программы, не «когда-нибудь потом», а строго в момент, когда функция заканчивает работу.

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

2. Когда срабатывает defer

С defer самая частая ошибка мышления такая: человек видит defer и подсознательно ожидает, что он выполнится «после текущей строки» (как будто это какой-то хитрый ;). Но defer не про строки. defer про выход из функции. Это означает, что deferred-вызов выполнится и при «нормальном» завершении, и при раннем return из любой ветки.

Посмотрим на маленький пример, где есть ранний выход. Здесь defer печатает строку "finish" независимо от того, вышли мы по ошибке или нет:

package main

import (
	"errors"
	"fmt"
)

func checkAge(age int) error {
	fmt.Println("start")          // start
	defer fmt.Println("finish")   // finish (выполнится при выходе)

	if age < 0 {
		return errors.New("age must be >= 0")
	}
	return nil
}

func main() {
	fmt.Println(checkAge(-10)) // age must be >= 0
}

Важно заметить, что "finish" печатается после того, как функция решила вернуть error, но до того, как управление окончательно ушло в вызывающий код.

Если вам хочется представить это визуально, то модель примерно такая:

flowchart TD
    A[выполняем тело функции] --> B{встречаем return?}
    B -->|нет| A
    B -->|да| C[выполняем deferred-вызовы LIFO]
    C --> D[функция реально возвращает значения]

Пока мы не добрались до return, defer «лежит в кармане» у функции и ждёт. Как только функция собралась уходить наружу — defer срабатывает.

В defer пишут вызов

Переходим к штуке, которая выглядит мелко, но бьёт по рукам регулярно. defer работает с вызовом функции. То есть правильная форма — defer something(...). Это важно, потому что defer должен знать, что именно вызывать, и с какими аргументами.

Вот корректный пример:

package main

import "fmt"

func say(msg string) {
	fmt.Println(msg)
}

func main() {
	defer say("bye")      // bye
	fmt.Println("hello")  // hello
}

А вот так нельзя:

// defer say  // так нельзя: defer ждёт вызов, а не имя

Это ограничение — часть модели: defer кладёт в стек «готовый вызов». И дальше мы подходим к главной теме лекции: когда именно фиксируются аргументы этого вызова и в каком порядке выполняются несколько defer.

3. Аргументы и порядок выполнения defer

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

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

Это означает, что defer fmt.Println(i) «запомнит» текущее значение i прямо сейчас, даже если дальше i поменяется десять раз.

Пример:

package main

import "fmt"

func main() {
	i := 0
	defer fmt.Println("defer i =", i) // defer i = 0
	i = 10
	fmt.Println("now i =", i)         // now i = 10
}

Если вы ожидали "defer i = 10", то вы ожидали неправильную модель. Здесь важно привыкнуть: defer фиксирует аргументы сразу, как фотография.

Мини-таблица «что вычисляется сейчас, а что потом»

Что происходит Когда происходит
Вычисляются аргументы f(x, y) в defer f(x, y) сразу, в момент выполнения строки с defer
Выполняется сам вызов f(...) позже, при выходе из функции
Порядок нескольких defer LIFO: последний добавленный выполнится первым

Несколько defer: порядок LIFO

Теперь второе правило, без которого defer превращается в гадание на кофейной гуще: если в функции несколько defer, они выполняются в обратном порядке. Это как стопка тарелок: последнюю положили сверху — её первой и забираем. Это и называют LIFO: Last In, First Out.

Вот пример, максимально честный и без философии:

package main

import "fmt"

func main() {
	defer fmt.Print("A") // выполнится третьим
	defer fmt.Print("B") // выполнится вторым
	defer fmt.Print("C") // выполнится первым
}

Результат будет таким:

CBA

Почему? Потому что «стек отложенных вызовов» работает по LIFO.

defer в цикле: отдельная ловушка

Здесь легко сделать неверный вывод: «ага, значит defer в цикле выполнится в конце итерации». Нет. Он выполнится в конце функции. А если вы в цикле поставили сто defer, то в конце функции получите сто вызовов (в обратном порядке). Это иногда то, что нужно, а иногда то, что неожиданно превращает программу в «печатный станок».

Покажем это на крошечном коде:

package main

import "fmt"

func main() {
	for i := 0; i < 3; i++ {
		defer fmt.Print(i) // 210
	}
}

На выходе вы увидите 210, потому что i=0 отложили первым (он внизу стопки), а i=2 — последним (он сверху), значит выполнится раньше.

defer и анонимная функция: где «прячется» замыкание

Частая ситуация: вы слышите «аргументы вычисляются сразу», а потом пишете код и видите «почему-то печатается последнее значение». Это не противоречие, просто здесь важно различать две вещи: аргументы вызова и замыкание, которое читает переменную позже.

Если вы делаете defer fmt.Println(i), то i — аргумент вызова, он вычисляется сразу.

Если вы делаете defer func() { fmt.Println(i) }(), то у этой анонимной функции нет аргументов, а i она берёт из внешней области видимости, то есть читает переменную в момент выполнения deferred-функции, а не в момент постановки defer.

Сравним два почти одинаковых фрагмента:

package main

import "fmt"

func main() {
	i := 0

	defer fmt.Println("arg:", i) // arg: 0

	defer func() {
		fmt.Println("closure:", i) // closure: 10
	}()

	i = 10
}

Здесь оба defer стоят «рядом», но ведут себя по-разному: fmt.Println("arg:", i) фиксирует i сразу и запоминает 0.

Анонимная функция замыкает переменную i и прочитает её позже, когда i уже 10.

Как сделать, чтобы анонимная функция «запомнила значение сейчас»

Нужно передать значение в параметр (то есть превратить «чтение из замыкания» в «аргумент вызова», который вычисляется сразу):

package main

import "fmt"

func main() {
	i := 0

	defer func(v int) {
		fmt.Println("v =", v) // v = 0
	}(i)

	i = 10
}

Это хороший паттерн, если вы хотите использовать defer с анонимной функцией, но при этом сохранить предсказуемость «значение фиксируется в момент defer».

4. Пример: мини‑калькулятор с defer

Сейчас привяжем defer к тому стилю программ, который у вас уже складывается с первых дней: читаем вход, парсим, считаем, печатаем. Мы не будем делать «реальный ресурс» вроде файла (это отдельная тема позже), но сделаем очень жизненную вещь: лог «вошли в функцию / вышли из функции». Такая штука помогает дебажить даже в простых задачах, и defer делает её почти бесплатной.

Представим, что у нас есть функция compute, которая делает операцию и может вернуть ошибку (деление на ноль, неизвестная операция). Мы хотим, чтобы в конце она всегда печатала "compute: end", даже если мы вышли по ошибке.

package main

import (
	"errors"
	"fmt"
)

func compute(op string, a, b int) (int, error) {
	fmt.Println("compute: start")     // compute: start
	defer fmt.Println("compute: end") // compute: end

	if op == "/" && b == 0 {
		return 0, errors.New("division by zero")
	}
	if op == "+" {
		return a + b, nil
	}
	if op == "/" {
		return a / b, nil
	}
	return 0, errors.New("unknown operation")
}

func main() {
	res, err := compute("/", 10, 0)
	fmt.Println("res =", res, "err =", err) // res = 0 err = division by zero
}

Главная идея примера: defer даёт нам гарантированный «хвост» функции. Мы не пишем fmt.Println("compute: end") вручную в каждом return. Мы пишем его один раз и забываем.

5. Ментальная модель стека defer

С defer очень помогает привычка «проигрывать» код в голове. Особенно на собеседованиях, но и просто в жизни — меньше сюрпризов. Для этого полезно держать одну простую картинку: у каждой функции есть свой внутренний стек deferred-вызовов. Когда встречаем defer, мы кладём новый вызов на вершину. Когда функция уходит наружу — начинаем снимать с вершины и выполнять.

Давайте специально напишем пример, где легко ошибиться, если не держать модель LIFO:

package main

import "fmt"

func demo() {
	defer fmt.Print("1") // выполнится третьим
	fmt.Print("A")       // выполнится сразу

	defer fmt.Print("2") // выполнится вторым
	fmt.Print("B")       // выполнится сразу

	defer fmt.Print("3") // выполнится первым
	fmt.Print("C")       // выполнится сразу
}

func main() {
	demo() // ABC321
}

Здесь полезно читать так: «Сейчас печатаем A, B, C. А потом, на выходе из demo(), печатаем 3, 2, 1». Итог: "ABC321".

Эта предсказуемость — причина, почему defer считается честным инструментом: он не требует телепатии, если вы знаете два правила (аргументы сразу, вызовы LIFO).

6. Типичные ошибки при работе с defer

Ошибка №1: ожидать, что defer выполнится «сразу после строки».
Такой баг часто появляется в голове после языков, где есть finally или где люди привыкли к «очистке в конце блока». В Go defer привязан к функции: пока функция не выходит, defer не исполняется. Если вы поставили defer в цикле, он не выполнится «в конце итерации» — он будет ждать выхода из функции целиком.

Ошибка №2: считать, что defer «возьмёт актуальные значения аргументов в конце».
Правило обратное: аргументы вычисляются сразу, в момент выполнения defer. Это особенно заметно на переменных, которые потом меняются. Если вам нужно, чтобы deferred-код увидел «последнее состояние», не передавайте это значение аргументом, а читайте его внутри замыкания — но делайте это осознанно, понимая разницу.

Ошибка №3: путать «аргументы defer» и «замыкание», а потом удивляться выводу.
Когда вы пишете defer fmt.Println(i), i фиксируется сразу. Когда вы пишете defer func(){ fmt.Println(i) }(), аргументов нет, и переменная читается позже. В результате один defer печатает «старое» значение, а другой — «новое», и это не баг Go, а разные механики.

Ошибка №4: ставить много defer в больших циклах и не думать о последствиях.
Да, defer удобен. Но если вы поставили десять тысяч defer внутри цикла, у вас будет десять тысяч отложенных вызовов, которые выполнятся только при выходе из функции. Даже без разговоров о производительности, это может просто сделать логи нечитаемыми и усложнить понимание порядка действий. Обычно в таких местах лучше либо переносить defer в функцию-обёртку на одну итерацию, либо явно делать действие в конце итерации (если именно это и нужно).

Ошибка №5: надеяться на «естественный порядок» вместо LIFO и получать обратный порядок.
Если вы открыли в голове модель «оно выполнится по порядку сверху вниз», то все примеры с defer будут казаться «сломанными». В Go порядок строго LIFO: последний defer выполняется первым. Это особенно важно, когда одно действие логически зависит от другого (например, если вы «взяли A, потом взяли B», то «отпускать» чаще хочется «сначала B, потом A» — и LIFO как раз делает это естественным).

1
Задача
Go SELF, 18 уровень, 0 лекция
Недоступна
Пропускной контроль
Пропускной контроль
1
Задача
Go SELF, 18 уровень, 0 лекция
Недоступна
Сохранённый чек
Сохранённый чек
1
Задача
Go SELF, 18 уровень, 0 лекция
Недоступна
Обратный отсчёт
Обратный отсчёт
1
Задача
Go SELF, 18 уровень, 0 лекция
Недоступна
Два повтора
Два повтора
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Vlad Tagunkov Уровень 24
23 мая 2026
последняя задача на версии 1.22+ при правильном коде выдает все равно - 3210 и валидацию не проходит.