defer, panic и recover в Go: три момента вместо одного и recover, который молча не работает
Собеседование идёт лестницей: когда выполняется отложенный вызов — когда вычисляются его аргументы — в каком порядке — почему отложенная функция может изменить результат — что recover ловит и чего не ловит — и во что всё это обходится. Замерено: открытый defer 4,68 нс против 16,8 нс на виток в цикле, а паника против возврата ошибки — в 122 раза.
Полное техническое изложение
TL;DR
defer — это не «выполнить в конце», а три разных момента: регистрация,
вычисление аргументов и выполнение. Отложенный вызов записывают сейчас, а
выполняют при выходе из функции; аргументы считаются в момент записи; несколько
отложенных вызовов идут в обратном порядке. Паника же разворачивает стек — и
перехватить её можно только из отложенного вызова.
Отсюда то, что выглядит не так, как читается код. i := 0; defer f(i); i = 42
напечатает 0: лечится обёрткой без аргументов, defer func() { f(i) }().
defer в цикле — не про цену, а про срок: он сработает на выходе из
функции, а не из витка. Отложенная функция может изменить результат — но
только именованный: замерено, именованный даёт 2, неименованный — 1. А
recover работает, только если вызван непосредственно в отложенной функции:
вынесли в помощник «для чистоты» — false против true в прогоне, и ни одного
предупреждения.
Дальше — числа, версии и границы. Один defer в функции — 4,68 нс (ноль
выделений); в цикле — 16,8 нс на виток; defer mu.Unlock() дороже ручного в
1,2 раза; паника против возврата ошибки — в 122 раза. Последняя
кратность — верхняя оценка: она снята на go1.24.7 на пустых функциях, где кроме
самой формы возврата нет никакой работы. Развёртывание defer открытым кодом —
свойство компилятора с Go 1.14, а не гарантия языка; panic(nil) с Go 1.21
превращается в *runtime.PanicNilError.
- функция что-то делает и рано или поздно возвращает управление тому, кто её вызвал;
- у ресурса есть парные операции: файл открывают и закрывают, мьютекс захватывают и освобождают;
- программа может аварийно завершиться, если пошло совсем не так.
- раскрутка стека, именованные результаты,
panicиrecover— что они делают, разбирается по дороге; - «открытый код» у
defer,*runtime.PanicNilError,fatal errorрантайма и наносекунды из замеров.
Что здесь на самом деле спрашивают
Лестница почти всегда такая:
- «Когда выполняется отложенный вызов?» — разминка, отвечают все.
- «А когда вычисляются его аргументы?» (с кодом) — здесь отваливается половина.
- «В каком порядке?» и сразу за ним «что будет с
deferв цикле?» - «Может ли отложенная функция изменить возвращаемое значение?» — вопрос про именованные результаты.
- «Что
recoverне поймает?» — проверяют, знаете ли вы про «непосредственно в отложенной функции» и про чужую горутину. - «
deferдорогой?» — вопрос на честность: без числа обе крайности одинаково плохи.
Дальше урок идёт по этой лестнице. Спина у неё одна: у отложенного вызова три момента, а не один.
База: три правила отложенного вызова и одно про панику
Прежде чем разбирать механизм, стоит назвать обычными словами то, из чего он
весь состоит. У defer три правила, и все три — про время.
Первое: отложенный вызов записывают сейчас, а выполняют при выходе из
функции. Строка defer f() сама ничего не вызывает. Она говорит: «когда эта
функция будет заканчиваться, позови f()». Заканчиваться — значит доходить до
возврата любым путём, в том числе через ошибку. Отсюда и польза: закрытие стоит
рядом с открытием, и забыть его нельзя, сколько бы return в функции ни было.
Второе: аргументы отложенного вызова вычисляются в момент записи, а не в
момент выполнения. defer f(i) запоминает не переменную i, а её значение —
то, какое было на этой строке. Изменится i потом — отложенный вызов об этом
не узнает. Это единственное место темы, где код читается не так, как
выполняется, и именно на нём чаще всего ошибаются.
Третье: несколько отложенных вызовов выполняются в обратном порядке. Записали три — выполнятся третий, второй, первый. Это ровно то, что нужно для парных операций: что захвачено последним, освобождается первым.
Теперь про панику. Паника не обрывает программу на месте — она разворачивает
стек: идёт от места сбоя вверх по вызовам и на каждом уровне по дороге
выполняет отложенные вызовы этого уровня. Поэтому defer надёжен и при панике:
файл закроется, мьютекс освободится.
И отсюда же ограничение, из которого растёт половина вопросов на собеседовании: перехватить панику можно только из отложенного вызова. Больше неоткуда — во время разворачивания выполняются только они; обычный код кадра к этому моменту уже позади.
Этого уже достаточно, чтобы ответить на базовый вопрос собеседования. Дальше —
про то, где эти три правила дают неожиданный результат: почему defer в цикле
держит открытыми сразу все файлы, почему отложенная функция может изменить
возвращаемое значение и почему перехват, вынесенный «для чистоты» в помощник,
молча перестаёт работать.
Механизм 1: одна временная шкала на три механизма
defer, panic и recover обычно объясняют по отдельности, и тогда их
приходится запоминать. Между тем это три точки одной шкалы — жизни вызова
функции:
Дальше урок проходит по этой шкале сверху вниз. Всё, что кажется в теме странным, — это следствие того, где на ней стоит та или иная точка.
Начнём с трёх моментов у самого defer:
- Регистрация — в момент выполнения строки с
defer. - Вычисление аргументов — тогда же, в момент регистрации.
- Выполнение — непосредственно перед возвратом из функции.
Второй пункт — это и есть вся разница между «знаю правило» и «знаю механизм». Спецификация формулирует его прямо:
Each time a "defer" statement executes, the function value and parameters to the
call are evaluated as usual and saved anew but the actual function is not
invoked.
Каждый раз при выполнении оператора defer значение функции и параметры вызова вычисляются как обычно и сохраняются заново, но сама функция не вызывается.
Переключайте сценарии и шаги — на каждом видно, какой из трёх моментов наступил:
Отсюда сразу три вывода, и каждый спрашивают отдельно.
Аргументы уже сняты. defer fmt.Println(i) печатает то значение, которое
i имело на строке с defer. Хотите позднее чтение — заверните: defer func() { fmt.Println(i) }(). Внутри литерала i читается в момент выполнения.
Получатель метода — тоже аргумент. defer mu.Unlock() снимает mu сразу,
и если переменную переприсвоят, отложенный вызов пойдёт к старому объекту.
Порядок обратный, потому что это стек. Освобождение идёт в порядке, обратном захвату: вложенное закрывается раньше объемлющего.
deferred functions are invoked immediately before the surrounding function
returns, in the reverse order they were deferred
Отложенные функции вызываются непосредственно перед возвратом из объемлющей функции, в порядке, обратном тому, в котором были отложены.
Механизм 2: defer в цикле — вопрос не цены, а срока
Классический вопрос «найдите ошибку», и код обычно такой:
func process(names []string) error {
for _, name := range names {
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close() // ЛОВУШКА
// …работа с f…
}
return nil
}Ошибка не в том, что это медленно. Ошибка в том, что f.Close() выполнится
на выходе из process, а не из витка. На тысяче файлов открытыми будут все
тысяча одновременно — и на каком-то из них программа упрётся в предел
дескрипторов.
Лечится выделением тела витка в функцию:
for _, name := range names {
if err := handle(name); err != nil { // defer внутри handle
return err
}
}Или, если выделять не хочется, — явным закрытием вместо отложенного. Второй
вариант хуже: он возвращает ровно ту проблему, ради которой defer и придуман.
Deferring a call to a function such as Close has two advantages. First, it
guarantees that you will never forget to close the file … Second, it means that
the close sits near the open, which is much clearer than placing it at the end of
the function.
Отложить вызов функции вроде Close даёт два преимущества. Во-первых, это гарантирует, что вы никогда не забудете закрыть файл. Во-вторых, закрытие оказывается рядом с открытием, что гораздо понятнее, чем размещать его в конце функции.
Механизм 3: изменить результат может только именованный
Вопрос звучит как «может ли defer повлиять на возвращаемое значение», и
правильный ответ — «да, если результат именован». Механизм такой: return x
— это не одно действие, а два. Сначала результату присваивается x, потом
выполняются отложенные вызовы, и только потом управление уходит вызывающему.
func named() (result int) {
defer func() { result *= 2 }()
return 1 // вернёт 2
}
func anon() int {
result := 1
defer func() { result *= 2 }()
return result // вернёт 1
}Разница не в стиле. В первом случае отложенная функция замыкается на сам результат; во втором — на локальную переменную, значение которой уже скопировано в результат.
if the deferred function is a function literal and the surrounding function has
named result parameters that are in scope within the literal, the deferred
function may access and modify the result parameters before they are returned
Если отложенная функция — функциональный литерал, а у объемлющей функции есть именованные результаты, видимые внутри литерала, отложенная функция может обратиться к результатам и изменить их до возврата.
Где это применяют на практике. Ровно в двух местах, и оба стоит назвать:
// 1. Превратить панику в ошибку на границе пакета.
func Parse(b []byte) (v Value, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("parse: %v", r)
}
}()
return parseFast(b), nil
}
// 2. Обернуть ошибку контекстом, не трогая каждый return.
func load(name string) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("load %s: %w", name, err)
}
}()
// …десяток return err…
}Оба приёма работают только с именованным результатом — и оба перестают работать молча, если имя убрать.
Механизм 4: паника — это раскрутка стека
Прежде чем разбирать, чего recover не ловит, стоит сказать, что происходит
при панике, — иначе все ограничения выглядят произвольными.
panic(v) не обрывает программу на месте. Он запускает раскрутку стека, и
на каждом кадре по дороге вверх происходит одно и то же:
Три вещи следуют отсюда прямо, и все три спрашивают.
Отложенные вызовы выполняются при панике. Это не «аварийный выход» — это
тот же момент 3 со шкалы первого механизма. Именно поэтому defer f.Close()
надёжен: файл закроется и при панике тоже.
Перехват возможен только на кадре, который ещё не раскручен. Раскрутка идёт
снизу вверх, и recover в кадре decode работает, а в кадре, который уже
пройден, — нет. Кадров ниже места паники к этому моменту уже не существует.
Если никто не перехватил, раскрутка доходит до верха горутины — и процесс завершается с трассировкой. Не «падает случайно»: доходит до конца по вполне определённому пути.
Чего recover не ловит
Здесь три ответа, и каждый спрашивают отдельно.
Первое — и самое частое. recover работает, только если вызван
непосредственно в отложенной функции. Не в функции, вызванной из неё.
Документация говорит это со скобками, и скобки тут — самая дорогая часть:
Executing a call to recover inside a deferred function (but not any function
called by it) stops the panicking sequence.
Вызов recover внутри отложенной функции (но не в какой-либо функции, вызванной из неё) останавливает последовательность паники.
Проверено запуском:
| форма | поймал |
|---|---|
recover() прямо в отложенной функции | true |
recover() вынесен в помощник | false |
Вторая строка — это тот самый рефакторинг «вынесем перехват в общий хелпер». Он компилируется, проходит ревью и отключает перехват.
Второе — паника другой горутины. У каждой горутины свой стек отложенных
вызовов, и снаружи её поймать нельзя: recover в вызывающем коде не увидит
ничего, а процесс умрёт. Спецификация говорит про «паникующую горутину», и
слово выбрано точно. Отсюда практическое правило: у каждой горутины, которую
вы запускаете, должен быть свой перехват, если её падение не должно ронять
процесс.
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("worker: %v", r)
}
}()
work()
}()Третье — fatal error рантайма. Одновременная запись в карту, дедлок всех
горутин, исчерпание памяти — это не паника. Стек не разматывается, отложенные
вызовы не выполняются, recover не вызывается вовсе.
И отдельно про panic(nil): с Go 1.21 он превращается в
*runtime.PanicNilError. До этого recover() возвращал nil, проверка
if r := recover(); r != nil не срабатывала, и функция продолжала работу
после паники — молча и с неверным результатом.
Механизм 5: паника — не способ вернуть ошибку
Отдельный вопрос, который задают под конец и который отделяет тех, кто писал на Go в проде.
Ошибка — это ожидаемый исход, часть контракта функции: файла нет, сеть недоступна, ввод некорректен. Такое возвращают значением, и вызывающий решает, что делать.
Паника — это нарушенный инвариант, то есть состояние, из которого продолжать
нельзя и о котором вызывающему сообщать бессмысленно: индекс вне границ,
разыменование nil, невозможная ветка switch. Стандартная библиотека паникует
ровно в таких местах.
Отсюда практическая граница, и она проверяемая:
| ситуация | чем сообщать |
|---|---|
| файл не открылся, запрос не прошёл, ввод неверен | значение error |
| нарушен инвариант, который автор считал невозможным | паника |
| ошибка в аргументах при инициализации пакета | паника допустима (regexp.MustCompile) |
«здесь слишком много if err != nil» | error; паника это не лечит |
Последняя строка — самая частая ошибка проектирования. Паника как способ избежать проверок делает поток управления невидимым: вызывающий не может понять по сигнатуре, что функция может не вернуться.
И одно правило про границы. Паника не должна пересекать границу пакета:
если внутри для удобства используется паника, на выходе её перехватывают и
превращают в error. Так устроен, например, разбор в encoding/json.
Глубже: что это стоит
Числа — из прогона bench/godefer/internals.go, чередующимися кругами.
У defer две разные цены, и в исходнике это одно и то же слово:
| время | выделений | |
|---|---|---|
один defer в функции | 4,68 нс | 0 |
defer в цикле, 8 витков | 16,8 нс на виток | 0 |
defer в цикле, 64 витка | 22,7 нс на виток | 0 |
Первая строка — «открытый» defer: с Go 1.14 компилятор разворачивает его в
обычный код на выходе из функции. Цикл этого не позволяет — число отложенных
вызовов заранее неизвестно, и каждый приходится регистрировать в списке.
Важная поправка к расхожему. Выделений в куче нет ни в одном случае —
запись об отложенном вызове кладётся на стек. Утверждение «defer в цикле
выделяет память» повторяют часто, и замер его не подтверждает.
На самом частом применении разница ещё меньше:
| время | |
|---|---|
defer mu.Unlock() | 28,48 нс |
mu.Unlock() вручную | 24,44 нс |
| кратность | 1,2 |
Паника — другой разговор:
| время | |
|---|---|
return err | 2,24 нс |
defer + recover, паники нет | 4,30 нс |
defer + recover, паника есть | 273,04 нс |
| паника против возврата | ×122 |
Важнее не четвёртая строка, а третья: форма с перехватом стоит дороже даже тогда, когда паники не случилось. Цену платит каждый вызов, а не только сбойный.
И у четвёртой строки надо назвать границу, иначе она превращается в закон. 122 — это отношение двух пустых функций на одной машине и одной версии Go: кроме самой формы возврата в них нет никакой работы, поэтому кратность здесь верхняя, а не типичная. Стоит добавить в обе функции хоть что-нибудь полезное — и она уменьшится, потому что уменьшается доля механики возврата в общем времени.
Поэтому правило пишется не от числа. Не «паника в сто раз дороже ошибки», а:
паникой не возвращают ожидаемый сбой — по причине, которая названа
механизмом 5 и от замера не зависит вовсе. Кратность нужна ровно для одного:
понять, что цена не в единицах процентов, и не пытаться выиграть на выборе между
error и паникой там, где выбор определяется смыслом.
Как отвечать на собеседовании
Короткий ответ: у отложенного вызова три момента, а не один. Регистрация и
вычисление аргументов — в момент defer, выполнение — при выходе из функции; а
если отложенных вызовов несколько, они идут в обратном порядке. Паника
разворачивает стек, выполняя по дороге отложенные вызовы каждого кадра, и
перехватить её можно только вызовом recover непосредственно в отложенной
функции.
Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.
Если интервьюер копает глубже
На «когда выполняется» отвечайте тремя моментами. «Регистрация и вычисление
аргументов — в момент defer, выполнение — перед возвратом». Второй пункт сразу
отделяет знающего механизм.
Про цикл говорите про срок, а не про скорость. «Сработает на выходе из функции, а не из витка; на тысяче файлов будет тысяча открытых дескрипторов». Ответ «это медленно» — не тот, которого ждут.
Про изменение результата назовите два действия return. «Сначала
присваивание результату, потом отложенные вызовы; поэтому именованный результат
изменить можно, а неименованный — нет».
Про recover процитируйте скобки. «Только непосредственно в отложенной
функции — не в вызванной из неё». И добавьте два случая, которые он не ловит:
чужая горутина и fatal error.
Про цену дайте два числа и разделите их. «Открытый defer — единицы
наносекунд и ноль выделений; в цикле — десятки на виток. Паника против возврата
ошибки — два порядка». Одно число без другого превращает верный замер в неверное
правило.
Дальше спросят
Выполнится ли defer, если функция паникует?
Да — в этом и смысл. При панике стек разматывается, и на каждом уровне
выполняются отложенные вызовы этого уровня. Именно поэтому defer mu.Unlock()
корректен и при панике: мьютекс освободится.
Не выполнятся они в двух случаях: при os.Exit (он завершает процесс, не
разматывая стек) и при fatal error рантайма. Отсюда практическое: os.Exit
в середине функции с отложенными вызовами — это тихая потеря всех очисток.
Что вернёт recover, если паники не было?
nil — и это законный способ проверки. Именно поэтому идиома пишется как
if r := recover(); r != nil, а не просто recover().
Ловушка была в том, что до Go 1.21 panic(nil) тоже давал nil, и такая
проверка пропускала настоящую панику. С 1.21 значение подменяется на
*runtime.PanicNilError, и проверка снова корректна. Старое поведение можно
вернуть через GODEBUG=panicnil=1 — знать про это стоит на случай чужого
легаси.
Стоит ли восстанавливаться после паники в HTTP-обработчике?
В net/http это уже сделано: сервер оборачивает каждый обработчик и при панике
закрывает соединение, а стек пишет в лог. Процесс при этом живёт.
Свой перехват имеет смысл, чтобы вернуть клиенту 500 вместо оборванного
соединения и записать панику в свои метрики. Но восстанавливаться и продолжать
обработку нельзя: паника означает нарушенный инвариант, и состояние после неё
не определено.
Как правильно проверять ошибку от Close?
defer f.Close() теряет её значение — и для чтения это нормально, а для записи
нет: ошибка закрытия у файла, открытого на запись, может означать, что данные не
дошли до диска.
Правильная форма использует именованный результат:
func write(name string) (err error) {
f, err := os.Create(name)
if err != nil {
return err
}
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = cerr
}
}()
// …запись…
}Условие err == nil здесь обязательно: иначе ошибка закрытия затрёт настоящую
причину сбоя.
Сколько отложенных вызовов можно накопить?
Жёсткого предела нет, ограничение — память. Но вопрос обычно про другое: про функцию, которая в цикле копит отложенные вызовы и оттого держит ресурсы.
Практический ориентир: если число defer в функции зависит от входных данных —
это почти всегда ошибка проектирования, а не оптимизации. Тело цикла должно
стать функцией, и тогда defer в нём снова станет открытым и сработает вовремя.
Чем panic отличается от исключений в других языках?
Механически — почти ничем: разматывание стека, обработчики, возможность перехватить. Отличается соглашение, и оно жёстче.
The convention in the Go libraries is that even when a package uses
panic internally, its external API still presents explicit error return
values
(В библиотеках Go принято, что даже когда пакет использует панику внутри, его внешний интерфейс всё равно возвращает явные значения ошибок). То есть паника — внутренний механизм, а наружу выходят ошибки.
Ответ «паника — это как исключение, просто пользуйтесь ей» не тот, которого
ждут.
Частые заблуждения
defer откладывает всё выражение целиком
Откладывается только вызов. Аргументы вычисляются в момент defer и сохраняются: i := 0; defer f(i); i = 42 передаст ноль. Позднее чтение получается обёрткой без аргументов — defer func() { f(i) }().
defer в цикле — это просто медленно
Дело не в скорости, а в сроке: отложенный вызов сработает на выходе из функции, а не из витка. Тысяча файлов, открытых в цикле, останутся открытыми все разом. Лечится выделением тела витка в функцию.
defer в цикле выделяет память в куче
Замер этого не подтверждает: выделений ноль и у открытого defer, и у defer в цикле — запись кладётся на стек. Дорожает регистрация в списке отложенных вызовов: 4,68 нс против 16,8 нс на виток.
отложенная функция не может повлиять на возвращаемое значение
Может — если результат именован. return x — это два действия: присвоить результату, потом выполнить отложенные вызовы. Замерено: именованный результат даёт 2, неименованный — 1. На этом стоит вся идиома превращения паники в ошибку.
recover можно вынести в общий вспомогательный метод
Нельзя, и это ломается молча. Документация: вызов recover работает внутри отложенной функции, «но не в какой-либо функции, вызванной из неё». Прогон подтверждает: прямой вызов ловит, вынесенный в помощник — нет.
recover в main поймает панику из любой горутины
Не поймает: у каждой горутины свой стек отложенных вызовов, и спецификация говорит про «паникующую горутину». Перехват нужен внутри самой горутины, иначе её паника уронит процесс.
recover спасёт от любой аварии
fatal error рантайма — одновременная запись в карту, дедлок всех горутин — не паника: стек не разматывается, отложенные вызовы не выполняются. И до Go 1.21 panic(nil) проходил мимо проверки r != nil, из-за чего функция продолжала работу после паники.
defer слишком дорогой для горячего кода
Смотря какой. Один defer в функции — 4,68 нс и ноль выделений; defer mu.Unlock() дороже ручного всего в 1,2 раза. Дорог не defer, а его форма в цикле — и там дело всё равно не в цене.
Практика
Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.
Практика · что напечатает
i := 0
defer fmt.Println(i)
i = 42
fmt.Println(i)
for n := 1; n <= 3; n++ {
defer fmt.Println(n)
}
func namedResult() (result int) {
defer func() { result *= 2 }()
return 1
}
func anonResult() int {
result := 1
defer func() { result *= 2 }()
return result
}Практика · оцените
Проверка знаний
i := 0; defer fmt.Println(i); i = 42. Что напечатает отложенный вызов?
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
defer— это не «выполнить в конце», а три разных момента: регистрация, вычисление аргументов и выполнение. Отложенный вызов записывают сейчас, а выполняют при выходе из функции; аргументы считаются в момент записи; несколько отложенных вызовов идут в обратном порядке. Паника же разворачивает стек — и перехватить её можно только из отложенного вызова.- Отсюда то, что выглядит не так, как читается код.
i := 0; defer f(i); i = 42напечатает 0: лечится обёрткой без аргументов,defer func() { f(i) }().deferв цикле — не про цену, а про срок: он сработает на выходе из функции, а не из витка. Отложенная функция может изменить результат — но только именованный: замерено, именованный даёт 2, неименованный — 1. Аrecoverработает, только если вызван непосредственно в отложенной функции: вынесли в помощник «для чистоты» —falseпротивtrueв прогоне, и ни одного предупреждения. - Дальше — числа, версии и границы. Один
deferв функции — 4,68 нс (ноль выделений); в цикле — 16,8 нс на виток;defer mu.Unlock()дороже ручного в 1,2 раза; паника против возврата ошибки — в 122 раза. Последняя кратность — верхняя оценка: она снята на go1.24.7 на пустых функциях, где кроме самой формы возврата нет никакой работы. Развёртываниеdeferоткрытым кодом — свойство компилятора с Go 1.14, а не гарантия языка;panic(nil)с Go 1.21 превращается в*runtime.PanicNilError.
На самом деле
- Откладывается только вызов. Аргументы вычисляются в момент
deferи сохраняются:i := 0; defer f(i); i = 42передаст ноль. Позднее чтение получается обёрткой без аргументов —defer func() { f(i) }(). - Дело не в скорости, а в сроке: отложенный вызов сработает на выходе из функции, а не из витка. Тысяча файлов, открытых в цикле, останутся открытыми все разом. Лечится выделением тела витка в функцию.
- Замер этого не подтверждает: выделений ноль и у открытого defer, и у defer в цикле — запись кладётся на стек. Дорожает регистрация в списке отложенных вызовов: 4,68 нс против 16,8 нс на виток.
- Может — если результат именован.
return x— это два действия: присвоить результату, потом выполнить отложенные вызовы. Замерено: именованный результат даёт 2, неименованный — 1. На этом стоит вся идиома превращения паники в ошибку. - Нельзя, и это ломается молча. Документация: вызов recover работает внутри отложенной функции, «но не в какой-либо функции, вызванной из неё». Прогон подтверждает: прямой вызов ловит, вынесенный в помощник — нет.
- Не поймает: у каждой горутины свой стек отложенных вызовов, и спецификация говорит про «паникующую горутину». Перехват нужен внутри самой горутины, иначе её паника уронит процесс.
fatal errorрантайма — одновременная запись в карту, дедлок всех горутин — не паника: стек не разматывается, отложенные вызовы не выполняются. И до Go 1.21panic(nil)проходил мимо проверкиr != nil, из-за чего функция продолжала работу после паники.- Смотря какой. Один
deferв функции — 4,68 нс и ноль выделений;defer mu.Unlock()дороже ручного всего в 1,2 раза. Дорог неdefer, а его форма в цикле — и там дело всё равно не в цене.
Что разобрано
- Что здесь на самом деле спрашивают
- База: три правила отложенного вызова и одно про панику
- Механизм 1: одна временная шкала на три механизма
- Механизм 2: defer в цикле — вопрос не цены, а срока
- Механизм 3: изменить результат может только именованный
- Механизм 4: паника — это раскрутка стека
- Механизм 5: паника — не способ вернуть ошибку
- Глубже: что это стоит
- Как отвечать на собеседовании
- Дальше спросят
- Частые заблуждения
- Практика
- Проверка знаний
Источники и что читать дальше
4 ИСТОЧНИКА
- Спецификация Go — Defer statements, Handling panicsОфициальная документация. Три правила, из которых выводится вся тема. Про аргументы: «Each time a "defer" statement executes, the function value and parameters to the call are evaluated as usual and saved anew but the actual function is not invoked» (Каждый раз при выполнении оператора defer значение функции и параметры вызова вычисляются как обычно и сохраняются заново, но сама функция не вызывается). Про порядок и момент: «deferred functions are invoked immediately before the surrounding function returns, in the reverse order they were deferred» (отложенные функции вызываются непосредственно перед возвратом из объемлющей функции, в порядке, обратном тому, в котором были отложены). И про результат: «if the deferred function is a function literal and the surrounding function has named result parameters that are in scope within the literal, the deferred function may access and modify the result parameters before they are returned» (если отложенная функция — функциональный литерал, а у объемлющей функции есть именованные результаты, видимые внутри литерала, отложенная функция может обратиться к результатам и изменить их до возврата). Про recover: «The recover function allows a program to manage behavior of a panicking goroutine» (Функция recover позволяет программе управлять поведением паникующей горутины) — и слово «горутины» здесь ключевое.https://go.dev/ref/spec#Defer_statements
- Пакет builtin — функция recoverОфициальная документация. Условие, которое чаще всего нарушают, записано в документации самой функции: «The recover function allows a program to manage behavior of a panicking goroutine. Executing a call to recover inside a deferred function (but not any function called by it) stops the panicking sequence» (Функция recover позволяет программе управлять поведением паникующей горутины. Вызов recover внутри отложенной функции (но не в какой-либо функции, вызванной из неё) останавливает последовательность паники). Скобки в этой фразе — самая дорогая часть темы. И там же про случай без паники: «if the goroutine is not panicking or recover was not called directly by a deferred function, recover returns nil» (если горутина не паникует или recover был вызван не непосредственно отложенной функцией, recover возвращает nil).https://pkg.go.dev/builtin#recover
- Effective Go — Defer, Panic, RecoverОфициальная документация. Про то, зачем defer нужен и почему момент выполнения именно такой: «Deferring a call to a function such as Close has two advantages. First, it guarantees that you will never forget to close the file … Second, it means that the close sits near the open, which is much clearer than placing it at the end of the function» (Отложить вызов функции вроде Close даёт два преимущества. Во-первых, это гарантирует, что вы никогда не забудете закрыть файл … Во-вторых, закрытие оказывается рядом с открытием, что гораздо понятнее, чем размещать его в конце функции). И граница применимости паники: «The convention in the Go libraries is that even when a package uses panic internally, its external API still presents explicit error return values» (В библиотеках Go принято, что даже когда пакет использует панику внутри, его внешний интерфейс всё равно возвращает явные значения ошибок).https://go.dev/doc/effective_go#defer
- Заметки к выпуску Go 1.21 — panic(nil)Официальная документация. Изменение, о котором спрашивают на собеседованиях с 2023 года: «In Go 1.21, panic(nil) now causes a run-time panic of type *runtime.PanicNilError» (В Go 1.21 panic(nil) теперь вызывает панику времени выполнения типа *runtime.PanicNilError). До этого recover возвращал nil, и проверка `if r := recover(); r != nil` пропускала такую панику молча — программа продолжала работу после паники.https://go.dev/doc/go1.21