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 как раз делает это естественным).
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ