defer -- откладывает вызов функции до выхода из текущей функции.

Ключевые моменты:

  • выполняется при любом выходе (return, panic, конец функции)
  • LIFO порядок (последний defer -- первый выполнится)
  • аргументы вычисляются сразу, в момент defer
  • closure захватывает переменную, не значение
  • можно изменить named return через defer

LIFO порядок:

go
defer fmt.Println("first")\ndefer fmt.Println("second") defer fmt.Println("third")\n // third, second, first

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

go
x := 10 defer fmt.Println(x) // вычисляется сейчас x = 20 // выведет 10

Closure -- захватывает переменную:

go
x := 10 defer func() { fmt.Println(x) }() x = 20 // выведет 20

Named return + defer:

go
func double() (result int) { result = 5 defer func() { result *= 2 }() return // вернет 10 }

Defer и panic/recover

Ключевые моменты:

  • выполняется даже при panic
  • recover работает только внутри defer
  • defer в цикле копится до выхода из функции
  • до Go 1.14 был дорогой (~35ns), сейчас почти бесплатный (~5ns)
  • open-coded defer работает только для простых случаев (не в цикле)

defer + panic:

go
func risky() { defer fmt.Println("cleanup") // выполнится\n panic("oops") }

panic + recover:

go
defer func() { if r := recover(); r != nil { fmt.Println("recovered:", r)\n } }() panic("oops") // программа продолжит работать

Осторожно с циклом:

go
for _, f := range files { file, _ := os.Open(f) defer file.Close() // копится до выхода из функции! } // 1000 файлов = 1000 открытых хендлов // решение: вынести в функцию for _, f := range files { processFile(f) // defer внутри закроется сразу }

Производительность:

text
до Go 1.14: ~35 ns (дорого) Go 1.14+: ~5 ns (open-coded defer) обычный вызов: ~4 ns open-coded работает если defer не в цикле

Внутреннее устройство defer

Связный список в структуре G. Каждый defer добавляется в голову (поэтому LIFO).

text
defer f1() → defer f2() → defer f3() G._defer → [f3] → [f2] → [f1] → nil head tail

Три реализации (эволюция):

ВерсияСпособСкорость
< Go 1.13Heap-allocated defer record~35 ns
Go 1.13Stack-allocated (если возможно)~15 ns
Go 1.14+Open-coded defer~5 ns

Open-coded defer: компилятор инлайнит defer как обычный код в конце функции. Битовая маска отслеживает какие defer'ы были зарегистрированы. Нет аллокации, нет связного списка.

Работает только для простых случаев — не в цикле, не больше 8 defer'ов в функции. Иначе fallback на stack/heap.


Defer и Mutex — подводные камни

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

go
// ❌ Мьютекс держится всю функцию func process() { mu.Lock() defer mu.Unlock() data := readSharedData() // критическая секция — 1 строка // ... 50 строк логики без shared data ... // всё это время мьютекс захвачен! } // ✅ Отпускаем сразу после критической секции func process() { mu.Lock() data := readSharedData() mu.Unlock() // ... 50 строк работают без блокировки }

Когда defer Unlock() нормально:

  • Вся функция — критическая секция
  • Функция короткая (3-5 строк)
  • Много return/panic — defer гарантирует Unlock

Практика

Quiz+10 XP

В каком порядке выполняются несколько defer в одной функции?

  • В порядке объявления (первый объявленный — первый выполнится)
  • Одновременно, порядок не определён
  • В обратном порядке объявления (последний объявленный — первый выполнится)
  • Зависит от типа функции
Predict+15 XP

Что выведет этот код?

go
package main import "fmt" func run() { defer fmt.Println("first")\n defer fmt.Println("second") defer fmt.Println("third")\n fmt.Println("start") } func main() { run() fmt.Println("end") }
Predict+15 XP

Что выведет этот код?

go
package main import "fmt" func double() (result int) { result = 5 defer func() { result *= 2 }() return } func main() { fmt.Println(double()) }
Исправь код+25 XP

Исправь код: файл должен закрываться всегда, даже если возникнет ошибка позже в функции. Используй defer.