defer -- откладывает вызов функции до выхода из текущей функции.
Ключевые моменты:
- выполняется при любом выходе (return, panic, конец функции)
- LIFO порядок (последний defer -- первый выполнится)
- аргументы вычисляются сразу, в момент defer
- closure захватывает переменную, не значение
- можно изменить named return через defer
LIFO порядок:
defer fmt.Println("first")\ndefer fmt.Println("second")
defer fmt.Println("third")\n
// third, second, first
Аргументы вычисляются сразу:
x := 10
defer fmt.Println(x) // вычисляется сейчас
x = 20
// выведет 10
Closure -- захватывает переменную:
x := 10
defer func() { fmt.Println(x) }()
x = 20
// выведет 20
Named return + defer:
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:
func risky() {
defer fmt.Println("cleanup") // выполнится\n panic("oops")
}
panic + recover:
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)\n }
}()
panic("oops")
// программа продолжит работать
Осторожно с циклом:
for _, f := range files {
file, _ := os.Open(f)
defer file.Close() // копится до выхода из функции!
}
// 1000 файлов = 1000 открытых хендлов
// решение: вынести в функцию
for _, f := range files {
processFile(f) // defer внутри закроется сразу
}
Производительность:
до Go 1.14: ~35 ns (дорого)
Go 1.14+: ~5 ns (open-coded defer)
обычный вызов: ~4 ns
open-coded работает если defer не в цикле
Внутреннее устройство defer
Связный список в структуре G. Каждый defer добавляется в голову (поэтому LIFO).
defer f1() → defer f2() → defer f3()
G._defer → [f3] → [f2] → [f1] → nil
head tail
Три реализации (эволюция):
| Версия | Способ | Скорость |
|---|---|---|
| < Go 1.13 | Heap-allocated defer record | ~35 ns |
| Go 1.13 | Stack-allocated (если возможно) | ~15 ns |
| Go 1.14+ | Open-coded defer | ~5 ns |
Open-coded defer: компилятор инлайнит defer как обычный код в конце функции. Битовая маска отслеживает какие defer'ы были зарегистрированы. Нет аллокации, нет связного списка.
Работает только для простых случаев — не в цикле, не больше 8 defer'ов в функции. Иначе fallback на stack/heap.
Defer и Mutex — подводные камни
defer выполняется при выходе из функции, не из блока. Мьютекс держится дольше чем нужно:
// ❌ Мьютекс держится всю функцию
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
Практика
В каком порядке выполняются несколько defer в одной функции?
Что выведет этот код?
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")
}
Что выведет этот код?
package main
import "fmt"
func double() (result int) {
result = 5
defer func() { result *= 2 }()
return
}
func main() {
fmt.Println(double())
}
Исправь код: файл должен закрываться всегда, даже если возникнет ошибка позже в функции. Используй defer.