💣 defer: Три ловушки, в которые попадают даже сеньоры — 18 августа 2026 г. в 11:26:56.811
💣 defer: Три ловушки, в которые попадают даже сеньоры Команда defer - это лучшее, что случалось с управлением ресурсами. Написал Lock(), тут же добавил defer Unlock(), и спишь спокойно. Больше никаких забытых закрытых файлов или коннектов к БД на ветках возврата с ошибкой. Но под этой элегантностью скрывается несколько неочевидных механизмов, которые регулярно взрывают продакшен. Давайте заглянем под капот и разберем классические ловушки. Миф: defer - это медленно Выходцы из старых версий Go (до 1.13) часто избегают defer в критичных к скорости участках кода, заявляя, что он тормозит (раньше каждый defer создавал структуру в куче). Забудьте об этом. Начиная с Go 1.14 компилятор делает Open-Coded Defers. Он статически встраивает вызов defer прямо перед каждым return на этапе компиляции. Сейчас накладные расходы на defer составляют ~1-2 наносекунды. Он практически бесплатный. Но есть нюансы. ❌ Ловушка 1: defer в цикле (Смерть через File Descriptors) Классика ревью. Джуниор пишет обработку пачки файлов: func ProcessFiles(files []string) error { for _, filename := range files { f, err := os.Open(filename) if err != nil { return err } defer f.Close() // 🤡 Бомба замедленного действия // Читаем файл... } return nil } В чем проблема? defer срабатывает при выходе из функции, а не из блока (как Drop в Rust или using в C#). Если в слайсе 10 000 файлов, цикл откроет их все одновременно, и только потом начнет закрывать. Вы гарантированно получите ошибку too many open files от операционной системы. А еще, defer в цикле не может быть оптимизирован компилятором (тот самый Open-Coded). Он будет аллоцироваться в куче, создавая мусор и замедляя работу. ✅ Решение: Выносить тело цикла в анонимную (или обычную) функцию: for _, filename := range files { func() { f, _ := os.Open(filename) defer f.Close() // Сработает сразу на каждой итерации // ... }() } ❌ Ловушка 2: Оценка аргументов в момент объявления Вы хотите замерить время работы функции: func DoWork() { start := time.Now() // 🤡 Выведет 0 миллисекунд defer fmt.Println("Время работы:", time.Since(start)) time.Sleep(2 * time.Second) } В чем проблема? Аргументы для отложенной функции вычисляются в момент объявления defer**, а не в момент её выполнения! time.Since(start) посчитается на первой же строчке, вернет 0, и defer запомнит это значение. ✅ Решение: Обернуть в замыкание. defer func() { fmt.Println("Время работы:", time.Since(start)) }() Тело анонимной функции выполнится в конце, и time.Since посчитается правильно. ❌ Ловушка 3: Игры с возвращаемыми значениями defer выполняется после того, как сработал return, но до того, как функция отдала управление вызывающему коду. Это позволяет творить черную магию, если использовать именованные возвращаемые переменные. func Magic() (result int) { defer func() { result++ // Меняем значение в последний момент! }() return 1 } Вызов Magic() вернет 2! Это не просто забавный фокус. Это стандартный паттерн для перехвата паник или оборачивания ошибок в самый последний момент: func DoDBTransaction() (err error) { defer func() { if err != nil { err = fmt.Errorf("transaction failed: %w", err) } }() // Если тут мы вернем ошибку, defer её обернет return db.Exec(...) } Кстати, defer работает по принципу LIFO (Last In, First Out) - как стек. Последний объявленный defer выполнится первым. Это логично: сначала мы лочим мьютекс, потом открываем файл, значит закрыть файл нужно до разлочки мьютекса. #golang #underhood #architecture #cleancode #bestpractices 👉 @golang_lib

