[{"content":"Когда я начинал изучать Go, одна конструкция показалась мне необычной. Это оператор defer. Несмотря на кажущуюся простоту, он скрывает достаточно нюансов, чтобы удивить даже опытного разработчика.\nЧто такое defer и зачем он нужен # defer позволяет отложить вызов функции до выхода из текущей функции. Неважно, как мы выходим: через return, по завершении тела или при панике. Отложенная функция всё равно будет вызвана. Удобно для очистки ресурсов.\nГде пригодится defer:\nОчистка ресурсов (закрытие файлов, соединений с БД, HTTP response body) Синхронизация (разблокировка мьютексов, ожидание WaitGroup) Обработка ошибок и паник Принудительная запись данных из буфера Измерение времени выполнения функций для метрик Три правила defer # Аргументы отложенной функции вычисляются в момент выполнения оператора defer, а не при фактическом вызове. 1 2 3 4 5 6 7 func main() { value := \u0026#34;old\u0026#34; defer fmt.Println(value) value = \u0026#34;new\u0026#34; } // Output: old Если нужно, чтобы defer увидел актуальное значение, используем замыкание. Оно захватывает переменную по ссылке:\n1 2 3 4 5 6 7 8 9 func main() { value := \u0026#34;old\u0026#34; defer func () { fmt.Println(value) }() value = \u0026#34;new\u0026#34; } // Output: new Отложенные функции вызываются в порядке LIFO (Last In First Out). 1 2 3 4 5 6 7 func main() { for i := range 10 { defer fmt.Print(i) } } // Output: 9876543210 Отложенные функции могут читать и изменять именованные возвращаемые значения функции. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func main() { value := getValue() fmt.Println(value) } func getValue() (value string) { defer func () { value = \u0026#34;new\u0026#34; // 2. value = \u0026#34;new\u0026#34; }() return \u0026#34;old\u0026#34; // 1. value = \u0026#34;old\u0026#34; } // 3. return \u0026#34;new\u0026#34; // Output: new Как это работает: return \u0026quot;old\u0026quot; сначала присваивает value = \u0026quot;old\u0026quot;, затем выполняются defer-функции (которые могут изменить value), и только потом функция реально возвращает значение.\ndefer, panic и recover # Перехватить панику в Go можно только через defer. Когда функция вызывает panic, нормальное выполнение останавливается. Go начинает раскрутку стека: поднимается по вызовам вверх и на каждом уровне выполняет все defer-функции в LIFO-порядке. Если ни одна из них не вызвала recover, программа завершается с выводом стектрейса.\n1 2 3 4 5 6 7 8 9 10 11 func main() { defer func() { if r := recover(); r != nil { fmt.Println(\u0026#34;recovered:\u0026#34;, r) } }() panic(\u0026#34;something went wrong\u0026#34;) } // Output: recovered: something went wrong Важная деталь: recover работает только при прямом вызове из defer-функции. Кажется логичным вынести обработку в хелпер, но это не сработает:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func myRecover() { if r := recover(); r != nil { fmt.Println(\u0026#34;recovered:\u0026#34;, r) } } func main() { defer myRecover() // panic не будет перехвачен panic(\u0026#34;crash\u0026#34;) } // Output: // panic: crash // // goroutine 1 [running]: // ... Ещё одна тонкость: паника распространяется только по стеку текущей горутины. Если горутина паникует без recover, падает вся программа, а не только она.\n1 2 3 4 5 6 7 8 9 func main() { go func() { panic(\u0026#34;crash\u0026#34;) // убьёт всю программу }() time.Sleep(time.Second) } // Output: panic: crash Ловушки и подводные камни # Игнорирование ошибки # 1 2 3 4 5 6 7 8 9 10 func writeData(filename string, data []byte) error { f, err := os.Create(filename) if err != nil { return err } defer f.Close() _, err = f.Write(data) return err } Выглядит правильно, но есть подвох. Если f.Close() вернёт ошибку, она потеряется. Чтобы её не потерять, делаем возвращаемую ошибку именованной и оборачиваем в defer. Благодаря третьему правилу defer-функция может прочитать err и даже изменить её.\n1 2 3 4 5 6 7 8 9 10 11 12 func writeData(filename string, data []byte) (err error) { f, err := os.Create(filename) if err != nil { return err } defer func() { err = errors.Join(err, f.Close()) }() _, err = f.Write(data) return err } Альтернатива: использовать f.Sync() перед return для записи, а defer f.Close() оставить только для закрытия дескриптора.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 func writeData(filename string, data []byte) error { f, err := os.Create(filename) if err != nil { return err } defer f.Close() _, err = f.Write(data) if err != nil { return err } return f.Sync() } errors.Join ловит все ошибки, но требует именованных возвращаемых значений. f.Sync() проще: не нужны ни замыкания, ни именованные значения. Разделение обязанностей: Sync() отвечает за данные, Close() за дескриптор.\ndefer внутри цикла # defer работает на уровне функции, не блока. В цикле это создаёт проблему:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func concatFiles(filenames ...string) (string, error) { var result string for _, filename := range filenames { f, err := os.Open(filename) if err != nil { return \u0026#34;\u0026#34;, err } defer f.Close() // все файлы закроются только при выходе из функции! data, err := io.ReadAll(f) if err != nil { return \u0026#34;\u0026#34;, err } result += string(data) } return result, nil } defer откладывает вызов f.Close() до выхода из функции concatFiles. Значит, открытые дескрипторы накапливаются, и все файлы закроются только в самом конце. Передадим слайс из 1000 filenames, и одновременно откроется 1000 дескрипторов. А ОС ограничивает их количество на процесс, обычно 1024 по умолчанию. На тысячу файлов может просто не хватить.\nКак это исправить:\nВынести чтение файла в отдельную функцию Не использовать defer, вызывать f.Close() явно Использовать анонимную функцию с немедленным вызовом (IIFE) Вот вариант с отдельной функцией:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func concatFiles(filenames ...string) (string, error) { var result string for _, filename := range filenames { data, err := readFile(filename) if err != nil { return \u0026#34;\u0026#34;, err } result += string(data) } return result, nil } func readFile(filename string) ([]byte, error) { f, err := os.Open(filename) if err != nil { return nil, err } defer f.Close() return io.ReadAll(f) } Value receiver в defer # Первое правило говорит: аргументы вычисляются в момент defer. Но receiver метода тоже аргумент, хотя по виду и не скажешь. Если метод принимает value receiver, struct копируется при регистрации defer:\n1 2 3 4 5 6 7 8 9 10 type Data struct{ a int } func (d Data) print() { fmt.Println(d.a) } func main() { d := Data{a: 10} defer d.print() // d копируется, выведет 10 d.a = 20 } // Output: 10 Исправляем pointer receiver\u0026rsquo;ом:\n1 2 3 func (d *Data) print() { fmt.Println(d.a) } // Output: 20 os.Exit() # Отложенная функция не вызывается, если программа завершается с помощью os.Exit(). Это может ударить по graceful shutdown. Если в main() настроены defer-функции для закрытия соединений или записи данных из буфера, а завершение идёт через os.Exit() (или log.Fatal, который вызывает os.Exit(1) под капотом), то ничего из этого не выполнится.\n1 2 3 4 5 6 func main() { defer fmt.Println(\u0026#34;deferred func\u0026#34;) os.Exit(0) } // Ничего не выводит Заключение # defer из тех механизмов Go, что просты на поверхности, но требуют понимания нюансов. Запомните три вещи: аргументы вычисляются при регистрации, а не при вызове; в циклах defer копит ресурсы до выхода из функции; ошибку Close() нельзя игнорировать при записи. Тогда defer станет надёжным инструментом, а не источником сюрпризов.\n","date":"30 июня 2026 г.","externalUrl":null,"permalink":"/ru/blog/defer-golang/","section":"Блог","summary":"Оператор defer прост на поверхности, но скрывает нюансы. Разбираем три правила, работу с panic и recover и типичные ловушки.","title":"defer в Go: правила и ловушки","type":"blog"},{"content":"","date":"30 июня 2026 г.","externalUrl":null,"permalink":"/ru/tags/go/","section":"Tags","summary":"","title":"Go","type":"tags"},{"content":"","date":"30 июня 2026 г.","externalUrl":null,"permalink":"/ru/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","externalUrl":null,"permalink":"/ru/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","externalUrl":"https://github.com/slavkluev/go-ticktick","permalink":"/ru/projects/go-ticktick/","section":"Проекты","summary":"Клиентская библиотека на Go для TickTick Open API. OAuth2-аутентификация, управление задачами и проектами.","title":"go-ticktick","type":"projects"},{"content":"","externalUrl":"https://github.com/slavkluev/go-yandex-tracker","permalink":"/ru/projects/go-yandex-tracker/","section":"Проекты","summary":"Клиентская библиотека на Go для API Яндекс Трекера. Задачи, очереди, массовые операции, аутентификация через OAuth и IAM. Без внешних зависимостей.","title":"go-yandex-tracker","type":"projects"},{"content":"Мой публичный ключ OpenPGP. Пишите мне зашифрованные письма и проверяйте мои подписи.\nОтпечаток\nB6AF 237D 7EBE 3DD9 48C1 A997 AB81 6E1A 9808 D27A\nСкачать ключ: pgp.asc Автообнаружение по email через WKD: hi@kliuev.dev На keyserver: keys.openpgp.org Сверяйте отпечаток выше по отдельному каналу, прежде чем доверять ключу.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -----BEGIN PGP PUBLIC KEY BLOCK----- mDMEaktxYRYJKwYBBAHaRw8BAQdAM9XhBKIW8T0iXhPi053B/dIYfowI4YPmSNBx XiBsc5m0IVZpYWNoZXNsYXYgS2xpdWV2IDxoaUBrbGl1ZXYuZGV2Poi1BBMWCgBd FiEEtq8jfX6+PdlIwamXq4FuGpgI0noFAmpLcWEbFIAAAAAABAAObWFudTIsMi41 KzEuMTIsMCwzAhsDBQkDwmcABQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJ EKuBbhqYCNJ6BsoA/3oElicLRgL5dg0c0vO2QQWP0J0C2HgcN7Sb+5uIB4pgAQD2 n8PqcxrVmlIadQWP5WmPC4v3lUAcvl+0TwSYBfZ1DLg4BGpLcWESCisGAQQBl1UB BQEBB0B1bTMvax6QU6fTbCDIfIb87/OxjyNwbnpYuu0Ch4ECLgMBCAeImgQYFgoA QhYhBLavI31+vj3ZSMGpl6uBbhqYCNJ6BQJqS3FhGxSAAAAAAAQADm1hbnUyLDIu NSsxLjEyLDAsMwIbDAUJA8JnAAAKCRCrgW4amAjSerxPAQDpiUvdnQn9/KBpirBi Tvxk9zgLRJa51DpF4qENn4YW4gD+IBxwwGLL3L451gMs2vvkwjOYNTyLLXkO/JHs QG1ZxgQ= =Z7wJ -----END PGP PUBLIC KEY BLOCK----- ","externalUrl":null,"permalink":"/ru/pgp/","section":"Главная","summary":"Мой публичный ключ OpenPGP. Пишите мне зашифрованные письма и проверяйте мои подписи.\nОтпечаток\nB6AF 237D 7EBE 3DD9 48C1 A997 AB81 6E1A 9808 D27A\nСкачать ключ: pgp.asc Автообнаружение по email через WKD: hi@kliuev.dev На keyserver: keys.openpgp.org Сверяйте отпечаток выше по отдельному каналу, прежде чем доверять ключу.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -----BEGIN PGP PUBLIC KEY BLOCK----- mDMEaktxYRYJKwYBBAHaRw8BAQdAM9XhBKIW8T0iXhPi053B/dIYfowI4YPmSNBx XiBsc5m0IVZpYWNoZXNsYXYgS2xpdWV2IDxoaUBrbGl1ZXYuZGV2Poi1BBMWCgBd FiEEtq8jfX6+PdlIwamXq4FuGpgI0noFAmpLcWEbFIAAAAAABAAObWFudTIsMi41 KzEuMTIsMCwzAhsDBQkDwmcABQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJ EKuBbhqYCNJ6BsoA/3oElicLRgL5dg0c0vO2QQWP0J0C2HgcN7Sb+5uIB4pgAQD2 n8PqcxrVmlIadQWP5WmPC4v3lUAcvl+0TwSYBfZ1DLg4BGpLcWESCisGAQQBl1UB BQEBB0B1bTMvax6QU6fTbCDIfIb87/OxjyNwbnpYuu0Ch4ECLgMBCAeImgQYFgoA QhYhBLavI31+vj3ZSMGpl6uBbhqYCNJ6BQJqS3FhGxSAAAAAAAQADm1hbnUyLDIu NSsxLjEyLDAsMwIbDAUJA8JnAAAKCRCrgW4amAjSerxPAQDpiUvdnQn9/KBpirBi Tvxk9zgLRJa51DpF4qENn4YW4gD+IBxwwGLL3L451gMs2vvkwjOYNTyLLXkO/JHs QG1ZxgQ= =Z7wJ -----END PGP PUBLIC KEY BLOCK----- ","title":"PGP","type":"page"},{"content":"","externalUrl":"https://github.com/slavkluev/ytr","permalink":"/ru/projects/ytr/","section":"Проекты","summary":"CLI Яндекс Трекера для людей и LLM-агентов. Управление задачами, массовые операции, структурированный JSON-вывод для скриптов и автоматизации.","title":"ytr","type":"projects"},{"content":"","externalUrl":null,"permalink":"/ru/blog/","section":"Блог","summary":"","title":"Блог","type":"blog"},{"content":"Пишу на Go. Рассказываю о бэкенде, инфраструктуре и пет-проектах.\n","externalUrl":null,"permalink":"/ru/","section":"Главная","summary":"Пишу на Go. Рассказываю о бэкенде, инфраструктуре и пет-проектах.\n","title":"Главная","type":"page"},{"content":"","externalUrl":null,"permalink":"/ru/projects/","section":"Проекты","summary":"","title":"Проекты","type":"projects"}]