Deep Engineering
Средний·Опубликовано·35 МИН

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 рантайма и наносекунды из замеров.

Что здесь на самом деле спрашивают

Лестница почти всегда такая:

  1. «Когда выполняется отложенный вызов?» — разминка, отвечают все.
  2. «А когда вычисляются его аргументы?» (с кодом) — здесь отваливается половина.
  3. «В каком порядке?» и сразу за ним «что будет с defer в цикле?»
  4. «Может ли отложенная функция изменить возвращаемое значение?» — вопрос про именованные результаты.
  5. «Что recover не поймает?» — проверяют, знаете ли вы про «непосредственно в отложенной функции» и про чужую горутину.
  6. «defer дорогой?» — вопрос на честность: без числа обе крайности одинаково плохи.

Дальше урок идёт по этой лестнице. Спина у неё одна: у отложенного вызова три момента, а не один.

База: три правила отложенного вызова и одно про панику

Прежде чем разбирать механизм, стоит назвать обычными словами то, из чего он весь состоит. У defer три правила, и все три — про время.

Первое: отложенный вызов записывают сейчас, а выполняют при выходе из функции. Строка defer f() сама ничего не вызывает. Она говорит: «когда эта функция будет заканчиваться, позови f()». Заканчиваться — значит доходить до возврата любым путём, в том числе через ошибку. Отсюда и польза: закрытие стоит рядом с открытием, и забыть его нельзя, сколько бы return в функции ни было.

Второе: аргументы отложенного вызова вычисляются в момент записи, а не в момент выполнения. defer f(i) запоминает не переменную i, а её значение — то, какое было на этой строке. Изменится i потом — отложенный вызов об этом не узнает. Это единственное место темы, где код читается не так, как выполняется, и именно на нём чаще всего ошибаются.

Третье: несколько отложенных вызовов выполняются в обратном порядке. Записали три — выполнятся третий, второй, первый. Это ровно то, что нужно для парных операций: что захвачено последним, освобождается первым.

Теперь про панику. Паника не обрывает программу на месте — она разворачивает стек: идёт от места сбоя вверх по вызовам и на каждом уровне по дороге выполняет отложенные вызовы этого уровня. Поэтому defer надёжен и при панике: файл закроется, мьютекс освободится.

И отсюда же ограничение, из которого растёт половина вопросов на собеседовании: перехватить панику можно только из отложенного вызова. Больше неоткуда — во время разворачивания выполняются только они; обычный код кадра к этому моменту уже позади.

Этого уже достаточно, чтобы ответить на базовый вопрос собеседования. Дальше — про то, где эти три правила дают неожиданный результат: почему defer в цикле держит открытыми сразу все файлы, почему отложенная функция может изменить возвращаемое значение и почему перехват, вынесенный «для чистоты» в помощник, молча перестаёт работать.

Механизм 1: одна временная шкала на три механизма

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

контракт языкаГарантии языка: порядок моментов задан спецификацией. Цена появится только в разделе «Глубже».

Дальше урок проходит по этой шкале сверху вниз. Всё, что кажется в теме странным, — это следствие того, где на ней стоит та или иная точка.

Начнём с трёх моментов у самого defer:

  1. Регистрация — в момент выполнения строки с defer.
  2. Вычисление аргументов — тогда же, в момент регистрации.
  3. Выполнение — непосредственно перед возвратом из функции.

Второй пункт — это и есть вся разница между «знаю правило» и «знаю механизм». Спецификация формулирует его прямо:

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 значение функции и параметры вызова вычисляются как обычно и сохраняются заново, но сама функция не вызывается.

Спецификация Go — Defer statements

Переключайте сценарии и шаги — на каждом видно, какой из трёх моментов наступил:

Отсюда сразу три вывода, и каждый спрашивают отдельно.

Аргументы уже сняты. 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

перевод

Отложенные функции вызываются непосредственно перед возвратом из объемлющей функции, в порядке, обратном тому, в котором были отложены.

Спецификация Go — Defer statements

Механизм 2: defer в цикле — вопрос не цены, а срока

Классический вопрос «найдите ошибку», и код обычно такой:

GO
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, а не из витка. На тысяче файлов открытыми будут все тысяча одновременно — и на каком-то из них программа упрётся в предел дескрипторов.

Лечится выделением тела витка в функцию:

GO
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 даёт два преимущества. Во-первых, это гарантирует, что вы никогда не забудете закрыть файл. Во-вторых, закрытие оказывается рядом с открытием, что гораздо понятнее, чем размещать его в конце функции.

Effective Go — Defer, Panic, Recover

Механизм 3: изменить результат может только именованный

Вопрос звучит как «может ли defer повлиять на возвращаемое значение», и правильный ответ — «да, если результат именован». Механизм такой: return x — это не одно действие, а два. Сначала результату присваивается x, потом выполняются отложенные вызовы, и только потом управление уходит вызывающему.

GO
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

перевод

Если отложенная функция — функциональный литерал, а у объемлющей функции есть именованные результаты, видимые внутри литерала, отложенная функция может обратиться к результатам и изменить их до возврата.

Спецификация Go — Defer statements

Где это применяют на практике. Ровно в двух местах, и оба стоит назвать:

GO
// 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 внутри отложенной функции (но не в какой-либо функции, вызванной из неё) останавливает последовательность паники.

Пакет builtin — функция recover

Проверено запуском:

формапоймал
recover() прямо в отложенной функцииtrue
recover() вынесен в помощникfalse

Вторая строка — это тот самый рефакторинг «вынесем перехват в общий хелпер». Он компилируется, проходит ревью и отключает перехват.

Второе — паника другой горутины. У каждой горутины свой стек отложенных вызовов, и снаружи её поймать нельзя: recover в вызывающем коде не увидит ничего, а процесс умрёт. Спецификация говорит про «паникующую горутину», и слово выбрано точно. Отсюда практическое правило: у каждой горутины, которую вы запускаете, должен быть свой перехват, если её падение не должно ронять процесс.

GO
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, go1.24.7 linux/amd64, два ядра. Числа сняты на конкретной машине и на пустых функциях: это верхняя оценка разницы.

Числа — из прогона bench/godefer/internals.go, чередующимися кругами.

У defer две разные цены, и в исходнике это одно и то же слово:

времявыделений
один defer в функции4,68 нс0
defer в цикле, 8 витков16,8 нс на виток0
defer в цикле, 64 витка22,7 нс на виток0

Первая строка — «открытый» defer: с Go 1.14 компилятор разворачивает его в обычный код на выходе из функции. Цикл этого не позволяет — число отложенных вызовов заранее неизвестно, и каждый приходится регистрировать в списке.

деталь реализации · Go 1.24«Открытый код», список отложенных вызовов и место записи о вызове — устройство нынешнего компилятора и рантайма, а не обещание языка. Спецификация задаёт только момент и порядок выполнения; во что это развернётся, менялось (открытый код появился в Go 1.14) и может измениться снова.

Важная поправка к расхожему. Выделений в куче нет ни в одном случае — запись об отложенном вызове кладётся на стек. Утверждение «defer в цикле выделяет память» повторяют часто, и замер его не подтверждает.

На самом частом применении разница ещё меньше:

время
defer mu.Unlock()28,48 нс
mu.Unlock() вручную24,44 нс
кратность1,2

Паника — другой разговор:

время
return err2,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() теряет её значение — и для чтения это нормально, а для записи нет: ошибка закрытия у файла, открытого на запись, может означать, что данные не дошли до диска.

Правильная форма использует именованный результат:

GO
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, а его форма в цикле — и там дело всё равно не в цене.

Практика

Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.

Практика · что напечатает

Аргументы отложенного вызова, порядок трёх 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
}

Практика · оцените

Критическая секция с одинаковой работой внутри. Во сколько раз defer mu.Unlock() дороже, чем mu.Unlock() вручную?
раза

Проверка знаний

Вопрос 1 из 6

i := 0; defer fmt.Println(i); i = 42. Что напечатает отложенный вызов?

Источники и что читать дальше

4 ИСТОЧНИКА

  1. Спецификация 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
  2. Пакет 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
  3. 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
  4. Заметки к выпуску 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