Перейти к содержимому

Defer, Panic, Recover: внутренности

Defer-panic-recover — это один из самых нетривиальных механизмов Go. На уровне Junior достаточно знать, что defer выполняется в LIFO порядке, а recover ловит panic. На Middle 2 от тебя ждут понимания: как именно работает open-coded defer (Go 1.14+), сколько ns overhead у defer на разных версиях, как именно происходит размотка стека при panic, в каких случаях recover не помогает. На собеседованиях это часто “трюковые” вопросы из категории “что напечатает код?”. Авито любит вопросы про порядок вычисления аргументов defer. Тинькофф — про panic в горутине. Яндекс — про open-coded defer optimization.

  1. Базовая концепция defer/panic/recover (для разогрева)
  2. Глубокое погружение: old defer, open-coded defer, panic internals
  3. Подводные камни
  4. Производительность и реальные кейсы
  5. Вопросы на собесе Middle 2
  6. Practice
  7. Источники

defer откладывает вызов функции до выхода из текущей функции. Гарантирует выполнение даже при panic.

func foo() {
defer fmt.Println("first")
defer fmt.Println("second")
return
}
// печатает: second, first (LIFO)

panic — управляемая ошибка. Поднимается через стек, размывая текущую функцию. Каждый defer на пути выполняется. Если нет recover — программа падает.

recover — функция, которая в defer ловит panic. Возвращает значение, переданное в panic(). Если panic нет — recover возвращает nil.

func safe() {
defer func() {
if r := recover(); r != nil {
log.Printf("recovered: %v", r)
}
}()
panic("boom")
}

Чем интересна реализация на Middle 2:

  • До Go 1.14 defer был дорогим (~50 ns), даже один defer — заметный overhead.
  • С Go 1.14 для “простых” defer’ов компилятор использует open-coded оптимизацию — стало ~1 ns.
  • Panic/recover остался сложным механизмом с _panic структурой и размоткой через _defer-список.

До 1.14 каждый defer foo(arg1, arg2) генерировал runtime-вызов:

// для каждого defer:
d := newdefer(siz) // аллокация _defer struct
d.fn = foo
d.sp = getsp()
d.pc = getpc()
// копирование arguments в d
d.link = g._defer
g._defer = d // вставляем в начало linked list
// при выходе из функции:
deferreturn() // вызывает все defer'ы LIFO

Структура _defer (runtime/runtime2.go):

type _defer struct {
started bool
heap bool
openDefer bool
sp uintptr // sp функции, в которой defer определён
pc uintptr
fn func()
_panic *_panic
link *_defer
fd unsafe.Pointer // funcdata
varp uintptr // value of varp for the stack frame
framepc uintptr
}

g._defer — голова intrusive linked list для текущей горутины. При выходе из функции deferreturn обходит этот список, выполняет и удаляет элементы.

⚠️ Каждый defer = аллокация _defer struct + копирование аргументов. Overhead ~30-50 ns даже для простого case. В hot path это было заметно.

Чтобы сократить аллокации, runtime держит pool в каждой P:

type p struct {
...
deferpool []*_defer
deferpoolbuf [32]*_defer
}

При newdefer сначала смотрим в deferpool. При исчерпании — берём из global pool с lock’ом. При создании совсем нового — heap allocation.

Это снизило overhead до ~20 ns в типичном случае.

Главная оптимизация: превратить defer в обычные function calls на этапе компиляции.

Условия применимости (все должны выполняться):

  • Не более 8 defer’ов в функции.
  • Defer не в цикле (for).
  • Defer не в условии (внутри if/switch/select) — это смягчили в более поздних версиях, теперь работает с условием.

Если условия выполнены — компилятор не использует runtime._defer вообще! Вместо этого:

  1. Аллоцирует на стеке bitmask (1 байт): какие defer’ы “активны” (выполнены ли уже).
  2. После defer-statement устанавливает бит “этот defer должен выполниться”.
  3. На return или panic обходит биты и вызывает соответствующие функции.

Псевдокод после компиляции:

func foo() {
var deferred uint8 = 0
// defer call1()
deferred |= 1
arg1_call1 := ... // аргументы вычисляются СЕЙЧАС, кладутся на стек
// defer call2(x, y)
deferred |= 2
arg1_call2 := x
arg2_call2 := y
// ... тело функции ...
// на return:
if deferred & 2 != 0 { call2(arg1_call2, arg2_call2) }
if deferred & 1 != 0 { call1(arg1_call1) }
}

Overhead — близкий к нулю (~1 ns на defer). Просто bitwise OR при defer-statement + проверки на return.

// 1. В цикле
for _, file := range files {
f, _ := os.Open(file)
defer f.Close() // ⚠️ classic defer
}
// 2. Более 8 defer'ов
defer a()
defer b()
defer c()
...
defer i() // 9-й — все идут в classic mode
// 3. Несколько разных типов в зависимости от branching (старые версии)
// (в новых — обычно работает)

Чтобы проверить, использует ли компилятор open-coded:

Окно терминала
go build -gcflags="-d=defer" main.go
# или
go build -gcflags="-m=2" main.go 2>&1 | grep defer

Old defer (linked list):

g._defer ─►┌────────┐ ─►┌────────┐ ─►┌────────┐ ─► nil
│ fn=baz │ │ fn=bar │ │ fn=foo │
│ sp=... │ │ sp=... │ │ sp=... │
│ link──►│ │ link──►│ │ link │
└────────┘ └────────┘ └────────┘
(LIFO: baz выполнится первым)

Open-coded (на стеке функции):

stack frame foo:
┌──────────────────┐
│ local vars │
├──────────────────┤
│ defer args │ ← аргументы defer'ов хранятся как локальные
│ call1: arg_a │
│ call2: arg_b,c │
│ call3: arg_d │
├──────────────────┤
│ deferred (1 byte)│ ← bitmask
│ = 0b00000111 │ (3 defer'а активны)
└──────────────────┘

LIFO (Last In, First Out):

func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
}
// вывод: 3, 2, 1

Это потому, что новый defer становится головой linked list (или ставит бит в bitmask). Выход из функции обходит сверху вниз.

2.7. Вычисление аргументов defer: ПРИ defer, а НЕ при выходе

Заголовок раздела «2.7. Вычисление аргументов defer: ПРИ defer, а НЕ при выходе»

ОЧЕНЬ ВАЖНОЕ ПРАВИЛО. Любимый вопрос на собесе.

func foo() {
x := 1
defer fmt.Println(x) // напечатает 1, не 10!
x = 10
}

Аргумент x вычисляется в момент defer, копируется на стек (или сохраняется в _defer struct). Изменения x после этого defer не видит.

Чтобы defer видел “текущее” значение — использовать closure:

func foo() {
x := 1
defer func() {
fmt.Println(x) // напечатает 10 (closure capture'ит x по ссылке)
}()
x = 10
}

⚠️ Для метода defer вычисляет receiver сразу:

func foo() {
s := "hello"
defer fmt.Println(s + " world") // печатает "hello world"
s = "goodbye"
// s изменилась, но defer вычислил аргумент раньше
}
func mypanic(arg interface{}) {
gp := getg()
var p _panic
p.arg = arg
p.link = gp._panic
gp._panic = &p
// обход defer-цепочки
for {
d := gp._defer
if d == nil { break }
if d.started {
// ранее запущенный defer, который сам panic'нул — пропускаем
...
}
d.started = true
d._panic = &p
reflectcall(d.fn, ...) // вызываем defer-функцию
if p.recovered {
// recover() сработал внутри defer
gp._panic = p.link
mcall(recovery) // прыжок назад на функцию, в которой был defer
}
gp._defer = d.link
freedefer(d)
}
// если ни один defer не recover'нул:
fatalpanic(&p) // печатает stack, exit
}

Структура _panic:

type _panic struct {
argp unsafe.Pointer
arg any // значение, переданное в panic()
link *_panic // предыдущий panic (вложенные)
pc uintptr
sp unsafe.Pointer
recovered bool
aborted bool
goexit bool
}
func recover() interface{} {
gp := getg()
p := gp._panic
if p == nil || p.goexit || p.aborted {
return nil // не в defer / уже отозван / Goexit
}
p.recovered = true
return p.arg
}

Recover работает ТОЛЬКО внутри defer (через проверку gp._defer != nil && gp._defer.started). Вне defer возвращает nil.

После recover():

  • p.recovered = true.
  • В panic loop: проверка после reflectcall(d.fn). Если p.recovered — прыжок назад на функцию через runtime.recovery.
  • recovery восстанавливает PC/SP из d.sp/d.pc, продолжает выполнение функции после оператора defer.

2.10. Recover внутри defer-функции (но не в самом её stmt)

Заголовок раздела «2.10. Recover внутри defer-функции (но не в самом её stmt)»

⚠️ Подводный камень:

func foo() {
defer recover() // НЕ РАБОТАЕТ
panic("boom")
}

Почему? Потому что recover это функция, вызванная через defer. Когда runtime обходит defer, он вызывает её, но это не “внутри defer” в смысле проверки. Recover проверяет: “был ли я вызван прямо из defer-функции? Тот ли это PC?”. Если recover напрямую как deferred — он вызван runtime’ом из reflectcall, не из user code.

Правильно:

func foo() {
defer func() {
if r := recover(); r != nil {
log.Println("recovered:", r)
}
}()
panic("boom")
}

Здесь recover() вызывается в user-функции, которая deferred. Runtime видит её на стеке выше recover’а и знает, что мы в “deferred context”.

go func() {
panic("boom") // паника в отдельной горутине
}()
defer func() {
recover() // ⚠️ НЕ ПОЙМАЕТ
}()
time.Sleep(time.Second)

Recover ловит panic в текущей горутине. Panic в другой goroutine = весь процесс умирает.

Правильно для goroutine:

go func() {
defer func() {
if r := recover(); r != nil {
log.Println("goroutine panic:", r)
}
}()
doRiskyWork()
}()

Это стандартная практика для long-running background workers — каждая горутина должна сама себя recover’ить.

До Go 1.21:

panic(nil) // panic с nil-значением
defer func() {
if r := recover(); r != nil {
// r == nil, не сработает!
}
}()

Это была долгая баг: recover() возвращает nil, и не различить — был ли panic с nil или вообще не было.

С Go 1.21:

// panic(nil) теперь паникует с *runtime.PanicNilError, не nil
defer func() {
if r := recover(); r != nil {
// теперь r будет *runtime.PanicNilError
}
}()

Для backward compatibility: можно вернуть старое поведение через GODEBUG=panicnil=1.

Bench (на amd64, Go 1.22):

BenchmarkOldDefer-8 50000000 35.0 ns/op
BenchmarkOpenCodedDefer-8 1000000000 0.9 ns/op

В 30-40 раз быстрее! Это значит, что в hot path с deferом overhead уже не критичен.

Когда переходим в old mode (defer в цикле, > 8) — overhead возвращается.

func processItems(items []Item) {
for _, item := range items {
f, err := os.Open(item.Path)
if err != nil { continue }
defer f.Close() // ⚠️ накапливается defer'ы для всех файлов!
// обрабатываем
}
// все файлы закрываются только в конце processItems
}

Проблема 1: file handles остаются открытыми до конца функции (leak ресурсов). Проблема 2: defer в цикле → classic mode → 30 ns × N items.

Решение:

func processItems(items []Item) {
for _, item := range items {
processOne(item) // отдельная функция, defer внутри неё
}
}
func processOne(item Item) {
f, err := os.Open(item.Path)
if err != nil { return }
defer f.Close() // open-coded, и closed после каждой итерации
// ...
}
defer func() {
if r := recover(); r != nil {
log.Println("first recovery:", r)
panic(r) // снова panic
}
}()
defer func() {
if r := recover(); r != nil {
log.Println("second recovery:", r) // тоже сработает!
}
}()
panic("boom")

После первого recover() — panic “снят”. Но мы сразу panic(r) снова → новый panic, который ловит второй defer. Это нормальный механизм, можно использовать для добавления контекста.

defer func() {
panic("from defer") // panic во время другого defer
}()
panic("original")

Что произойдёт? Original panic поднимается, начинаем обходить defer-цепь. Defer выше вызывает panic(“from defer”). В runtime panic loop: новый panic не отменяет старый, оба связаны через _panic.link.

При fatal print будут оба:

panic: original [recovered]
panic: from defer
goroutine ...

То есть видна цепочка panic’ов.

runtime.Goexit() завершает текущую goroutine, выполняя все её defer’ы.

Отличие от panic:

  • Defer’ы выполняются (как panic).
  • Recover не отменяет Goexit (даже если defer вызвал recover, горутина всё равно завершится).
  • Не печатает stack.
go func() {
defer func() {
if r := recover(); r != nil {
log.Println("recovered:", r) // не сработает
}
}()
runtime.Goexit()
}()

В этом коде defer выполнится, но recover вернёт nil (нет panic). Goroutine всё равно умрёт.

При fatal panic Go печатает stack trace:

panic: my error
goroutine 1 [running]:
main.foo()
/tmp/foo.go:10 +0x68
main.main()
/tmp/foo.go:14 +0x20
exit status 2

В горутинах: [chan receive], [sleep], [running] — текущее состояние.

При SIGQUIT (Ctrl+) Go печатает stack всех горутин — стандартная функция runtime’а.

func f1() {
defer fmt.Println("f1 defer")
f2()
}
func f2() {
defer fmt.Println("f2 defer")
defer func() {
recover() // ловит panic в f2
fmt.Println("f2 recovered")
}()
panic("from f2")
}

Что выведется?

f2 recovered
f2 defer
f1 defer

Recover в f2 ловит panic. f2 продолжает выполнение остальных defer’ов (fmt.Println("f2 defer")), потом нормально возвращается. f1 не паникует, выполняет свой defer.

⚠️ Recover остаётся в той функции, где defer его вызвал. F1 ничего не знает о panic’е.

AspectGo (defer/panic/recover)Java/C# (try/catch)
ИдиоматичностьPanic для exceptional, errors для ожидаемыхException для всего
Скорость catchОчень дорого (размотка)Дёшево (jump)
Скорость throw~µs (panic)µs (throw + stack capture)
ScopeТолько текущая goroutineThreads
Cleanupdeferfinally
Cancellationcontext.ContextThread.Interrupt()

Идиома Go: panic ТОЛЬКО для “невосстановимых” ситуаций (внутренние invariant’ы программы). Для обычных ошибок — return error.

Стандартный паттерн:

func RecoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
stack := make([]byte, 4<<10)
stack = stack[:runtime.Stack(stack, false)]
log.Printf("panic in handler: %v\n%s", rec, stack)
http.Error(w, "Internal Server Error", 500)
}
}()
next.ServeHTTP(w, r)
})
}

Это критично для production HTTP сервисов. Без recovery middleware один panic-handler уронит весь pod.

⚠️ Стандартный http.Server уже имеет встроенный recovery (печатает в log, отвечает 500). Но он не настраиваемый. Своё middleware обычно нужно для structured logging.

func BenchmarkNoDefer(b *testing.B) {
var m sync.Mutex
for i := 0; i < b.N; i++ {
m.Lock()
m.Unlock()
}
}
func BenchmarkDeferMu(b *testing.B) {
var m sync.Mutex
for i := 0; i < b.N; i++ {
m.Lock()
defer m.Unlock() // ⚠️ defer'ы накапливаются для b.N итераций!
}
}

Второй бенч сделает b.N defer’ов, все они выполнятся в конце функции. Это и баг (mutex держится), и overhead (heap-аллокация _defer на каждую итерацию).

Правильный бенч с defer:

func BenchmarkDeferMu(b *testing.B) {
var m sync.Mutex
for i := 0; i < b.N; i++ {
func() {
m.Lock()
defer m.Unlock() // open-coded, locked/unlocked в каждой итерации
}()
}
}
  • В очень тёмных hot path (миллионы ops/sec) — даже open-coded ~1 ns × млн = 1 ms.
  • Если простая операция (один Close) — иногда obj.Close() в конце явно читабельнее.
  • В коротких функциях с гарантированным single exit path.

Но в большинстве случаев — defer лучше и читабельнее.


Самый известный gotcha:

defer fmt.Println("at start:", time.Now())
// ... 5 секунд работы ...
// напечатает время в момент defer, НЕ в момент return

Для late evaluation — closure:

defer func() { fmt.Println("at end:", time.Now()) }()
for _, f := range files {
fd, _ := os.Open(f)
defer fd.Close() // НЕ закрываются на каждой итерации!
}

Закроются ВСЕ в конце функции. Если файлов 10000 — открытыми будет 10000 fd → может ulimit.

Решение: extract в отдельную функцию или явный fd.Close() в конце итерации.

func foo() {
if recover() != nil { ... } // НЕ работает (не в defer)
}

Recover работает ТОЛЬКО внутри defer-функции. Вне — возвращает nil.

defer recover() // НЕ работает

Это запускает recover как deferred-функцию. Recover проверяет PC своего вызывателя, и runtime-context не подходит.

Правильно — wrap в closure:

defer func() { recover() }()

Любая необработанная panic в горутине = весь процесс умирает.

go func() {
// если что-то паникнет здесь без defer/recover — RIP service
}()

Best practice: каждая background-goroutine начинается с recover-defer.

func foo() (result int) {
defer func() {
result++ // изменяет возвращаемое значение!
}()
return 1
}
// returns 2

Named return value + closure-defer = можно модифицировать return. Хитрый трюк, иногда используется в error wrapping:

func foo() (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("foo: %w", err)
}
}()
return doStuff()
}
var m map[string]int
m["a"] = 1 // panic: assignment to entry in nil map

Это runtime panic, не возвращается через error. Если хочется recoverить — обязательный defer + recover.

defer func() {
if r := recover(); r != nil {
switch v := r.(type) {
case error:
log.Println("error:", v)
case string:
log.Println("string:", v)
case runtime.Error:
log.Println("runtime:", v)
}
}
}()

panic(value) принимает any. Recover тоже возвращает any.

Panic обходит defer-список. Если у тебя 100 defer’ов в стеке — это 100 reflectcall’ов. На hot path это редко важно, но в exception-heavy code (что в Go ан inderior) можно заметить.

go func() {
defer cleanup() // выполнится
runtime.Goexit()
unreachable()
}()

Goexit отрабатывает defer’ы, но завершает горутину. Полезно: например, в test’е t.FailNow() использует Goexit, что выполняет cleanup defer’ы.

func init() {
defer fmt.Println("init done") // выполнится после init()
setup()
}

Это работает, но обычно бессмысленно. init() вызывается один раз, перед main. Defer там добавляет 30 ns на инициализацию.

Невозможна. _defer — это intrusive linked list, добавляем в начало. Циклы создать нельзя через нормальное defer-statement.

defer func() {
recover()
}()
panic("boom")
// функция возвращает нормально, как будто panic не было

Если хочешь обработать и продолжить panic — recover() + panic(r):

defer func() {
if r := recover(); r != nil {
log.Println("logged:", r)
panic(r) // продолжаем
}
}()

panic в init() функции = программа не запустится. Recovery не сработает (нет main-горутины ещё для catch).

3.15. ⚠️ Open-coded defer не применяется при defer-statement в go или defer

Заголовок раздела «3.15. ⚠️ Open-coded defer не применяется при defer-statement в go или defer»

Если defer-statement сам нестабилен (например, depends on runtime condition), компилятор может сдаться и переключиться в classic mode. Проверка через -gcflags="-m=2".


Кейс (Yandex 2019, до 1.14): Profile показывал runtime.deferreturn в топ-5. Defer был “налогом” на каждую функцию. Команда обсуждала, не убрать ли defer из hot path вообще.

После апгрейда на Go 1.14 — overhead defer’а упал в 30x. Profile показал runtime.deferreturn < 0.1%. Дискуссия закрыта.

Кейс (Ozon batch processor): процесс обрабатывал 100K файлов, каждый открывался + defer Close. Все 100K fd оставались открытыми. После 5К — too many open files.

Решение: вынесли обработку одного файла в helper-функцию.

Кейс (Avito, продакшен): миграция на новую версию протокола привела к panic в новом codec’е. Без recovery middleware — каждый bad request убивал бы pod, OOM-killer перезагружал.

С recovery middleware: pod продолжал работать, ошибки шли в лог, alarm срабатывал.

Кейс (gRPC worker pool): воркеры в pool получали jobs через chan. Один баг — job == nil — приводил к panic. Без recover — вся горутина-воркер умирала, pool деградировал.

Решение: каждый worker начинался с defer-recover, ошибка логировалась, worker продолжал работу.

Кейс: микросервис добавлял контекст в каждую ошибку через паттерн:

func (s *Service) DoWork(ctx context.Context) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("DoWork: %w", err)
}
}()
return s.internal(ctx)
}

Это автоматизирует error wrapping. Производительный кейс — open-coded defer + 30-40 ns на функцию.

Кейс (legacy сервис на Go 1.20): panic(nil) где-то в codecs, recovery возвращал nil → программа считала “всё ок”, но в реальности был баг.

Решение: апгрейд на Go 1.21 (panicnil автоматически конвертится в *runtime.PanicNilError).

Стандартный паттерн:

func handler(w, r) {
start := time.Now()
defer func() {
log.Printf("handler took %v", time.Since(start))
}()
// ...
}

Overhead — open-coded ~1 ns. Гораздо меньше, чем сам log.Printf (микросекунды).


Q1. Что напечатает код?

func main() {
x := 1
defer fmt.Println(x)
x = 10
}

Напечатает 1. Аргумент defer’а вычисляется сразу — x = 1 сохраняется. Изменение x = 10 после этого не влияет.

Q2. А этот?

func main() {
x := 1
defer func() {
fmt.Println(x)
}()
x = 10
}

Напечатает 10. Closure capture’ит x по ссылке (на стек или escape), на момент выполнения defer’а x = 10.

Q3. Что такое open-coded defer?

Оптимизация Go 1.14+. Компилятор для функций с ≤ 8 defer’ов, не в цикле, не в условии — превращает defer в обычные function calls с bitmask на стеке. Overhead падает с ~30 ns до ~1 ns.

Q4. Когда open-coded не работает?

  • defer внутри цикла.
  • 8 defer’ов в функции.

  • Очень сложный control flow (старые версии).

Q5. Опиши структуру _defer.

type _defer struct { started bool; sp, pc uintptr; fn func(); _panic *_panic; link *_defer; ... }. Intrusive linked list через link. Лежит на heap (или в pool) для classic mode, на стеке для open-coded.

Q6. Опиши, что происходит при panic.

  1. Создаётся _panic struct, цепляется к g._panic.
  2. Runtime обходит g._defer linked list (или open-coded bitmask) LIFO.
  3. Для каждого defer’а: выполняем функцию через reflectcall.
  4. Если defer вызвал recover() → _panic.recovered = true, прыжок назад в исходную функцию.
  5. Если ни один defer не recover’нул → fatalpanic (print stack, exit).

Q7. Опиши, что делает recover().

Проверяет, есть ли активный panic в текущей горутине (gp._panic) и вызывается ли из контекста defer. Если да — устанавливает p.recovered = true, возвращает p.arg. Иначе — nil.

Q8. Почему defer recover() не работает?

recover() проверяет своего caller’а — он должен быть defer-функцией пользователя. Когда defer recover() — recover вызывается напрямую runtime’ом из reflectcall, не из user-функции. Поэтому recover видит, что он не “в defer context” и возвращает nil.

Q9. Что произойдёт, если defer вызовет panic?

Новый panic добавляется в цепочку через _panic.link. Старый не отменяется. При fatal print будут оба:

panic: original [recovered]
panic: new

Q10. Можно ли recover panic в другой горутине?

Нет. Recover работает только в текущей горутине. Panic в goroutine A не виден из defer-функции в goroutine B. Каждая goroutine должна сама себя recover’ить.

Q11. Что такое panic(nil) проблема и как её решает Go 1.21?

До 1.21: panic(nil) → recover() возвращает nil → невозможно отличить “был panic с nil” от “не было panic”. В 1.21 panic(nil) автоматически конвертится в *runtime.PanicNilError, recover вернёт это значение.

Q12. Что произойдёт при runtime.Goexit()?

Текущая goroutine завершается. Все её defer’ы выполняются. Recover внутри defer вернёт nil (нет panic) и не отменит Goexit. Если Goexit в main горутине — программа выходит как os.Exit(0).

Q13. Чем Goexit отличается от panic + recover?

Goexitpanic + recover
Defer’ы выполняютсяВыполняются
Recover не работаетРаботает (отменяет panic)
Голос горутина умираетМожет продолжить (recover ловит)
Не печатает stackПечатает (если не recover)

Q14. Сколько ns overhead у defer на Go 1.22?

Open-coded (≤8 defer’ов, не в цикле): ~1 ns. Classic mode (в цикле или > 8): ~30-50 ns.

Q15. Что напечатает?

func foo() (result int) {
defer func() { result++ }()
return 1
}
fmt.Println(foo())

Напечатает 2. Named return value + closure-defer. return 1 сначала записывает 1 в result, потом defer выполняется и инкрементит до 2.

Q16. Можно ли изменять не-named return value через defer?

Нет. Если return value не named, мы не знаем переменной, которую модифицировать. Defer может изменить только named return values.

Q17. Опиши паттерн error wrapping через defer.

func foo() (err error) {
defer func() {
if err != nil { err = fmt.Errorf("foo: %w", err) }
}()
return bar()
}

Использует named return + closure. После return bar() runtime записывает err, выполняет defer, который оборачивает err.

Q18. Что такое recovery middleware и зачем?

HTTP middleware с defer-recover, ловящий panic’и в handler’ах. Без него один panic убивает весь сервер. С ним — ошибка логируется, клиент получает 500.

Q19. Что произойдёт, если в HTTP handler сделать panic(“oops”)?

Стандартный http.Server имеет встроенный recovery: панику ловит, печатает в log (или server ErrorLog), отвечает клиенту “500 Internal Server Error”. Но без своего middleware ты не контролируешь формат логирования.

Q20. Где defer overhead важен?

Если в hot loop ~1M ops/sec — даже 1 ns defer × 1M = 1 ms (1% от секунды). На latency-критичных путях иногда убирают defer. Но для большинства кода — overhead незначим.

Q21. Какой порядок выполнения defer’ов?

LIFO (last in, first out). Последний defer выполняется первым.

Q22. Что вернёт recover() в нормальном коде (без panic)?

nil. Вне panic-context recover возвращает nil.

Q23. Что произойдёт при panic в init()?

init() функция упадёт с panic, программа не запустится (main не вызывается). Recovery в main не поможет, потому что main ещё не стартовала.

Q24. Можно ли использовать defer для гарантии разблокировки mutex?

Да, это идиоматичный паттерн:

mu.Lock()
defer mu.Unlock()

Гарантия: даже при panic mutex разблокируется (выполнится defer).

Q25. Как defer взаимодействует с return statement?

return value это два действия:

  1. Запись value в named return (если есть) или временный слот.
  2. Запуск defer’ов.
  3. Выход из функции.

Defer’ы могут изменить named return value между шагом 1 и 3.

Q26. Что такое stack trace при panic?

При fatal panic Go печатает stack каждой горутины. Главная (где panic) — с маркером [running]. Это runtime.dopanic делает.

Q27. Что произойдёт при SIGQUIT (Ctrl+)?

Go runtime ловит SIGQUIT, печатает stack всех горутин (как при panic), завершает программу. Полезно для debug “что зависло”.

Q28. Сколько байт занимает _defer на heap?

~64 байта на amd64 (struct fields). Плюс аргументы — копируются в дополнительной памяти.

Q29. Что такое cooperative panic?

Не Go-термин. В Go panic всегда “preemptive” в смысле — сразу размывает стек. Нет такого, что panic ждёт удобного момента.

Q30. Что напечатает?

func main() {
defer fmt.Println("a")
defer fmt.Println("b")
panic("boom")
}
b
a
panic: boom
goroutine 1 [running]:
...

Defer’ы LIFO, потом fatal panic print.

Q31. Что произойдёт, если recover вернёт значение, и мы его проигнорируем?

Если recover() != nil — panic считается обработанным. Программа продолжается после функции, в которой был defer. Игнорируем — значит логика просто не использует значение, но recovery всё равно сработала.


Напиши код:

func main() {
for i := 0; i < 3; i++ {
defer fmt.Println(i)
}
}

Что напечатает? Объясни порядок.

(Ответ: 2, 1, 0 — LIFO, и каждый i вычислен при defer’е, значения 0, 1, 2 сохранены, выполняются в обратном порядке.)

func main() {
for i := 0; i < 3; i++ {
defer func() { fmt.Println(i) }()
}
}

Что напечатает в Go ≤ 1.21? А в Go 1.22+? Объясни.

(До 1.22 — три раза 3 (closure capture’ит i по ссылке, к моменту выполнения i = 3). С 1.22 каждая итерация for создаёт новую i (новая семантика loop variable), и closure capture’ит каждый по-своему — напечатает 2, 1, 0.)

Напиши два бенчмарка:

  1. Функция с одним defer (внутри тела).
  2. Функция с тем же defer внутри for-loop.

Замерь ns/op. Объясни разницу (open-coded vs classic).

Реализуй HTTP middleware:

  • Recover panic.
  • Log stack trace.
  • Return 500.
  • Set unique request-id в log.

Напиши worker pool, где каждый worker имеет deferred recover. Покажи, что panic в одном job’е не убивает worker.

Реализуй pattern:

func DoSomething(ctx context.Context) (err error) {
defer func() {
if err != nil { err = fmt.Errorf("DoSomething: %w", err) }
}()
// ...
}

Сравни с явным wrapping без defer.

Напиши:

type Resource struct { Name string }
func (r *Resource) Close() { fmt.Println("closing", r.Name) }
func main() {
r := &Resource{Name: "first"}
defer r.Close()
r = &Resource{Name: "second"}
}

Что напечатает? Объясни, как обращение к receiver работает в defer.

(Напечатает “closing first”. Receiver вычисляется при defer’е — это указатель на первый Resource. Переприсваивание r создаёт новую переменную, defer держит старую.)

Напиши код с двумя вложенными panic’ами (panic в defer во время другого panic). Покажи, что print показывает оба.


  1. src/runtime/panic.go — реализация panic, recover, defer.
  2. src/runtime/runtime2.go — структуры _defer, _panic.
  3. Dan Scales, “Proposal: Low-cost defers through inline code, and extra funcdata to manage the panic case” — Go 1.14 design doc.
  4. Keith Randall, “Go 1.13 sync.Pool and defer optimization” — GopherCon 2019.
  5. Cherry Zhang, “Go 1.14: Async preempt + open-coded defer” — talk.
  6. “Defer optimization in Go 1.14” — Habr / Yandex Engineering Blog 2020.
  7. Go 1.21 release notes — про panic(nil) → PanicNilError.
  8. “panic, recover and defer in Go” — Dave Cheney blog 2018.
  9. “Goroutine, panic, recovery: best practices” — Ardan Labs blog.
  10. “Effective Go” — официальный doc, секция про defer/panic/recover.
  11. Russ Cox, “Defer Statement” — design doc 2008.
  12. “Avoiding panic-driven design” — Brad Fitzpatrick talk, GopherCon EU 2018.