Go's defer and panic: Open-Coded Defers and the Runtime's Unwind Loop
A defer in Go is either a bit in a byte on the stack, a record in the goroutine's defer chain, or a heap object, and the panic machinery walks those three shapes frame by frame.
TL;DR: A
deferin Go is one of three things: a bit in a byte-sized mask on the stack plus a function pointer in a slot (“open-coded”), a_deferrecord in the goroutine’s defer chain (“stack-allocated”), or a_deferrecord on the heap (“heap-allocated”). Which one you get depends on where thedeferstatement sits.panicthen walks the stack looking for those three shapes in order, running deferred calls as it goes.
Go’s defer looks like a language feature. It is really three different
mechanisms wearing the same syntax, and the runtime keeps them apart.
The distinction matters for two reasons. First, it is the difference between a
defer that costs about a nanosecond and one that costs seventy. Second, it is
the reason a defer inside a loop behaves differently from a defer outside
one, even though the source looks identical.
This is a walk through all of it, based on reading the Go 1.28 development tree
(src/runtime/panic.go, src/runtime/runtime2.go, and the compiler passes in
src/cmd/compile/internal/).
The runtime’s own summary
The runtime opens with a comment that is worth reading in full, because it is the design in prose:
// src/runtime/panic.go:38
// We have two different ways of doing defers. The older way involves creating a
// defer record at the time that a defer statement is executing and adding it to a
// defer chain. This chain is inspected by the deferreturn call at all function
// exits in order to run the appropriate defer calls. A cheaper way (which we call
// open-coded defers) is used for functions in which no defer statements occur in
// loops. In that case, we simply store the defer function/arg information into
// specific stack slots at the point of each defer statement, as well as setting a
// bit in a bitmask. At each function exit, we add inline code to directly make
// the appropriate defer calls based on the bitmask and fn/arg information stored
// on the stack. During panic/Goexit processing, the appropriate defer calls are
// made using extra funcdata info that indicates the exact stack slots that
// contain the bitmask and defer fn/args.Note the constraint in the middle: open-coded defers are used “for functions in
which no defer statements occur in loops”. That single clause is the source of
almost every surprise people have about defer performance.
The three kinds of defer
| Kind | Where the state lives | Allocated when | Cost |
|---|---|---|---|
| Open-coded | a bit in a uint8 mask + a function value in a stack slot | compile time | roughly 1 ns |
| Stack-allocated | a _defer record in the goroutine’s defer chain | the defer statement executes | roughly 35 ns |
| Heap-allocated | a _defer record allocated on the heap | the defer statement executes | roughly 50-70 ns |
The rough nanosecond figures are the numbers the Go wiki has published for
years; they are not stated anywhere in the source tree, so treat them as
orders of magnitude rather than measurements. The tree’s own benchmarks are
BenchmarkDefer (open-coded), BenchmarkDefer10 (a defer in a loop) and
BenchmarkDeferMany, all in src/runtime/runtime_test.go. BenchmarkDefer10
is the interesting one: the ten defers in the loop are all live at once, and
the loop is exactly what knocks the function off the open-coded path.
The _defer record
The non-open-coded kinds share one record type. The runtime defines it, and the compiler mirrors the layout exactly:
// src/runtime/runtime2.go:1150
// A _defer holds an entry on the list of deferred calls.
// If you add a field here, add code to clear it in deferProcStack.
// This struct must match the code in cmd/compile/internal/ssagen/ssa.go:deferstruct
// and cmd/compile/internal/ssagen/ssa.go:(*state).call.
// Some defers will be allocated on the stack and some on the heap.
// All defers are logically part of the stack, so write barriers to
// initialize them are not required. All defers must be manually scanned,
// and for heap defers, marked.
type _defer struct {
heap bool
rangefunc bool // true for rangefunc list
sp uintptr // sp at time of defer
pc uintptr // pc at time of defer
fn func() // can be nil for open-coded defers
link *_defer // next defer on G; can point to either heap or stack!
// If rangefunc is true, *head is the head of the atomic linked list
// during a range-over-func execution.
head *atomic.Pointer[_defer]
}The sp and pc fields are the whole trick for unwinding: they record where
the defer statement was, so the runtime can tell which deferred calls belong
to the frame it is currently unwinding. The link field makes the chain a
singly-linked list hanging off g._defer.
The compiler builds a byte-for-byte identical struct for the stack-allocated
case, in cmd/compile/internal/ssagen/ssa.go:deferstruct, with one
instruction worth noting: the pointer-carrying fields are declared as
uintptr, because defer records are scanned explicitly during stack copying
and by the GC rather than through the type’s pointer map.
Open-coded defers: one bit each
The fast path is entirely compile-time. The compiler reserves a one-byte
variable, deferBits, in the function’s frame:
// src/cmd/compile/internal/ssagen/ssa.go:449
if s.hasOpenDefers {
// Create the deferBits variable and stack slot. deferBits is a
// bitmask showing which of the open-coded defers in this function
// have been activated.
deferBitsTemp := typecheck.TempAt(src.NoXPos, s.curfn, types.Types[types.TUINT8])
deferBitsTemp.SetAddrtaken(true)
s.deferBitsTemp = deferBitsTemp
// For this value, AuxInt is initialized to zero by default
startDeferBits := s.entryNewValue0(ssaop.OpConst8, types.Types[types.TUINT8])
s.vars[deferBitsVar] = startDeferBits
s.deferBitsAddr = s.addr(deferBitsTemp)
s.store(types.Types[types.TUINT8], s.deferBitsAddr, startDeferBits)One byte, so eight defers maximum. That limit is enforced in the walker, and the reason is stated plainly:
// src/cmd/compile/internal/walk/walk.go:306
// The max number of defers in a function using open-coded defers. We enforce this
// limit because the deferBits bitmask is currently a single byte (to minimize code size)
const maxOpenDefers = 8When the defer statement executes, the compiler stores the function value
into a dedicated stack slot and sets one bit:
// src/cmd/compile/internal/ssagen/ssa.go:4868
// Update deferBits only after evaluation and storage to stack of
// the function is successful.
bitvalue := s.constInt8(types.Types[types.TUINT8], 1<<uint(index))
newDeferBits := s.newValue2(ssaop.OpOr8, types.Types[types.TUINT8], s.variable(deferBitsVar, types.Types[types.TUINT8]), bitvalue)
s.vars[deferBitsVar] = newDeferBits
s.store(types.Types[types.TUINT8], s.deferBitsAddr, newDeferBits)So the runtime data structure for a function with four defers is:
frame of f()
+----------------------+
| deferBits: uint8 | bit 0 = defer #1 armed
| | bit 1 = defer #2 armed
| | ...
+----------------------+
| slot 0: func() | closure for defer #1
| slot 1: func() | closure for defer #2
| slot 2: func() | closure for defer #3
| slot 3: func() | closure for defer #4
+----------------------+At every return site the compiler emits inline code that loads the byte, tests each bit in reverse order, and calls the matching slot. No call to the runtime at all, no allocation, no list.
The deferreturn trampoline
Functions that use the non-open-coded kinds end their body with a call to a runtime trampoline:
// src/runtime/panic.go:655
// deferreturn runs deferred functions for the caller's frame.
// The compiler inserts a call to this at the end of any
// function which calls defer.
func deferreturn() {
var p _panic
p.deferreturn = true
p.start(sys.GetCallerPC(), unsafe.Pointer(sys.GetCallerSP()))
for {
fn, ok := p.nextDefer()
if !ok {
break
}
fn()
}
}Notice that deferreturn reuses the panic machinery. It builds a _panic
value with deferreturn set, and then runs the same nextDefer loop that
gopanic runs. That is not an accident of implementation; it is how the
runtime keeps one implementation of “run the deferred calls for this frame”.
The trampoline’s address is recorded by the linker, per function, so that the
panic unwinder can find it later. src/cmd/link/internal/ld/pcln.go computes
it in computeDeferReturn, and the runtime reads it back out of the function
metadata.
Open-coded functions get a deferreturn call too, but a disconnected one:
the compiler emits a call to the trampoline and a return that are never
executed on the normal path, purely so that a deferred function which
recovers a panic has somewhere to return to:
// src/cmd/compile/internal/ssagen/ssa.go:7282
if openDeferInfo != nil {
// When doing open-coded defers, generate a disconnected call to
// deferreturn and a return. This will be used to during panic
// recovery to unwind the stack and return back to the runtime.That is why initOpenCodedDefers can insist on a non-zero deferreturn
offset: it is the address the runtime jumps back to when a recover happens
inside an open-coded defer.
Stack-allocated and heap-allocated defers
When open-coding is off, the compiler emits a call to one of two runtime
helpers. The stack variant is used when the compiler can prove the defer’s
arguments do not escape:
// src/cmd/compile/internal/ssagen/ssa.go:1738
if s.hasOpenDefers {
s.openDeferRecord(n.Call.(*ir.CallExpr))
} else {
d := callDefer
if n.Esc() == ir.EscNever && n.DeferAt == nil {
d = callDeferStack
}
s.call(n.Call.(*ir.CallExpr), d, false, n.DeferAt)
}callDeferStack builds a _defer record in the function’s own frame and
hands its address to the runtime:
// src/runtime/panic.go:538
// deferprocStack queues a new deferred function with a defer record on the stack.
// The defer record must have its fn field initialized.
// All other fields can contain junk.
// Nosplit because of the uninitialized pointer fields on the stack.
//
//go:nosplit
func deferprocStack(d *_defer) {
gp := getg()
if gp.m.curg != gp {
// go code on the system stack can't defer
throw("defer on system stack")
}
// fn is already set.
// The other fields are junk on entry to deferprocStack and
// are initialized here.
d.heap = false
d.rangefunc = false
d.sp = sys.GetCallerSP()
d.pc = sys.GetCallerPC()
// The lines below implement:
// d.link = gp._defer
// d.head = nil
// gp._defer = d
// But without write barriers. The first two are writes to
// the stack so they don't need a write barrier, and furthermore
// are to uninitialized memory, so they must not use a write barrier.
// The third write does not require a write barrier because we
// explicitly mark all the defer structures, so we don't need to
// keep track of pointers to them with a write barrier.
*(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))
*(*uintptr)(unsafe.Pointer(&d.head)) = 0
*(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))
}The heap variant is deferproc, which allocates the record from a per-P pool
and links it onto the goroutine’s chain:
// src/runtime/panic.go:351
// Create a new deferred function fn, which has no arguments and results.
// The compiler turns a defer statement into a call to this.
func deferproc(fn func()) {
gp := getg()
if gp.m.curg != gp {
// go code on the system stack can't defer
throw("defer on system stack")
}
d := newdefer()
d.link = gp._defer
gp._defer = d
d.fn = fn
d.pc = sys.GetCallerPC()
// We must not be preempted between calling GetCallerSP and
// storing it to d.sp because GetCallerSP's result is a
// uintptr stack pointer.
d.sp = sys.GetCallerSP()
}and the allocation comes from a pool, not from mallocgc. newdefer in
src/runtime/panic.go:578 takes a _defer from the current P’s
deferpool, refilling it from the central sched.deferpool under
sched.deferlock when it runs dry, and only falls back to new(_defer) –
the actual heap allocation – when both pools are empty. The records are
returned to the pool by popDefer when the defer runs.
Why a defer in a loop kills open-coding
This is the rule people trip over. The escape analysis pass is what decides,
and it uses loopDepth to detect the loop:
// src/cmd/compile/internal/escape/call.go:237
// goDeferStmt analyzes a "go" or "defer" statement.
func (e *escape) goDeferStmt(n *ir.GoDeferStmt) {
k := e.heapHole()
if n.Op() == ir.ODEFER && e.loopDepth == 1 && n.DeferAt == nil {
// Top-level defer arguments don't escape to the heap,
// but they do need to last until they're invoked.
k = e.later(e.discardHole())
// force stack allocation of defer record, unless
// open-coded defers are used (see ssa.go)
n.SetEsc(ir.EscNever)
}loopDepth == 1 means “directly in the function body”. Inside a loop the
depth is greater, the EscNever marking never happens, and the defer ends
up on the heap path.
There are three more ways to lose open-coding, all in one place in the walker:
// src/cmd/compile/internal/walk/stmt.go:106
case ir.ODEFER:
n := n.(*ir.GoDeferStmt)
w.curfunc.SetHasDefer(true)
w.curfunc.NumDefers++
if w.curfunc.NumDefers > maxOpenDefers || n.DeferAt != nil {
// Don't allow open-coded defers if there are more than
// 8 defers in the function, since we use a single
// byte to record active defers.
// Also don't allow if we need to use deferprocat.
w.curfunc.SetOpenCodedDeferDisallowed(true)
}
if n.Esc() != ir.EscNever {
// If n.Esc is not EscNever, then this defer occurs in a loop,
// so open-coded defers cannot be used in this function.
w.curfunc.SetOpenCodedDeferDisallowed(true)
}So open-coding is off when: there are more than eight defers, the defer is
in a loop, the defer needs the range-over-func mechanism, or the function
is too complicated for the inline exit code (the SSA pass also bails out when
NumReturns*NumDefers > 15, or when the function has heap-allocated results,
or when race instrumentation is on).
Panic: the unwind loop
panic is not a special control flow mechanism. It is a loop that runs
deferred calls, frame by frame, until it runs out of them or somebody
recovers.
// src/runtime/panic.go:799
// The implementation of the predeclared function panic.
// The compiler emits calls to this function.
//
// gopanic should be an internal detail,
// but historically, widely used packages access it using linkname.
//
// Do not remove or change the type signature.
// See go.dev/issue/67401.
//
//go:linkname gopanic
func gopanic(e any) {
if e == nil {
if debug.panicnil.Load() != 1 {
e = new(PanicNilError)
} else {
panicnil.IncNonDefault()
}
}and then the loop:
// src/runtime/panic.go:848
var p _panic
p.arg = e
runningPanicDefers.Add(1)
p.start(sys.GetCallerPC(), unsafe.Pointer(sys.GetCallerSP()))
for {
fn, ok := p.nextDefer()
if !ok {
break
}
fn()
}The interesting part is nextDefer, because it is where the three defer
kinds come together. It checks the open-coded mask first, then the defer
chain:
// src/runtime/panic.go:944
for {
for p.deferBitsPtr != nil {
bits := *p.deferBitsPtr
// Check whether any open-coded defers are still pending.
//
// Note: We need to check this upfront (rather than after
// clearing the top bit) because it's possible that Goexit
// invokes a deferred call, and there were still more pending
// open-coded defers in the frame; but then the deferred call
// panic and invoked the remaining defers in the frame, before
// recovering and restarting the Goexit loop.
if bits == 0 {
p.deferBitsPtr = nil
break
}
// Find index of top bit set.
i := 7 - uintptr(sys.LeadingZeros8(bits))
// Clear bit and store it back.
bits &^= 1 << i
*p.deferBitsPtr = bits
return *(*func())(add(p.slotsPtr, i*goarch.PtrSize)), true
}
Recheck:
if d := gp._defer; d != nil && d.sp == uintptr(p.sp) {
if d.rangefunc {
deferconvert(d)
popDefer(gp)
goto Recheck
}
fn := d.fn
p.retpc = d.pc
// Unlink and free.
popDefer(gp)
return fn, true
}
if !p.nextFrame() {
return nil, false
}
}Read the open-coded branch carefully. sys.LeadingZeros8 finds the highest
set bit, which is the last defer to run, because defers run in reverse
order. The bit is cleared before the call so that if the deferred function
panics, this defer is not run a second time.
The chain branch checks d.sp == uintptr(p.sp): the deferred call belongs to
the frame currently being unwound. If the top of the chain belongs to a
different frame, nextFrame walks the stack up one frame and tries again.
Finding the open-coded mask during a panic
During a panic, the runtime is not running the function’s exit code, so it
cannot read deferBits from a known place. Instead it uses funcdata recorded
by the compiler:
// src/runtime/panic.go:1059
func (p *_panic) initOpenCodedDefers(fn funcInfo, varp unsafe.Pointer) bool {
fd := funcdata(fn, abi.FUNCDATA_OpenCodedDeferInfo)
if fd == nil {
return false
}
if fn.deferreturn == 0 {
throw("missing deferreturn")
}
deferBitsOffset, fd := readvarintUnsafe(fd)
deferBitsPtr := (*uint8)(add(varp, -uintptr(deferBitsOffset)))
if *deferBitsPtr == 0 {
return false // has open-coded defers, but none pending
}
slotsOffset, fd := readvarintUnsafe(fd)
p.retpc = fn.entry() + uintptr(fn.deferreturn)
p.deferBitsPtr = deferBitsPtr
p.slotsPtr = add(varp, -uintptr(slotsOffset))
return true
}The funcdata itself is two varints – the offset of the deferBits byte and
the offset of the first closure slot – as documented in
cmd/compile/internal/ssagen/ssa.go:emitOpenDeferInfo. Two offsets and
eight bits: that is the entire runtime description of an open-coded defer
set.
recover only works in a deferred function
recover is a very small function:
// src/runtime/panic.go:1084
// The implementation of the predeclared function recover.
func gorecover() any {
p := recoverablePanic(3) // skip systemstack_switch, recoverablePanic, gorecover
if p == nil {
return nil
}
p.recovered = true
return p.arg
}The work is in recoverablePanic, which has to answer one question: is the
function calling recover the function that the panicking function deferred?
The runtime walks the stack from the recover call up to gopanic, counting
frames that are not wrappers, and the answer is “there must be exactly one
non-wrapper frame between gopanic and gorecover”:
// src/runtime/panic.go:1179
// So our criteria is, there must be exactly one non-wrapper
// frame between gopanic and gorecover.
//
// We don't recover this:
//
// defer func() { func() { recover() }() }()
//
// because there are 2 non-wrapper frames.
//
// We don't recover this:
//
// defer recover()
//
// because there are 0 non-wrapper frames.
//
// It must not be inlined into its callers: they count on its frame being
// there to skip.
//
//go:noinline
func recoverablePanic(skip int) *_panic {
gp := getg()
p := gp._panic
if p == nil || p.goexit || p.recovered {
return nil
}
canRecover := false
systemstack(func() {
var u unwinder
u.init(gp, 0)
for i := 0; i < skip; i++ {
u.next()
}
nonWrapperFrames := 0
loop:
for ; u.valid(); u.next() {
for iu, f := newInlineUnwinder(u.frame.fn, u.symPC()); f.valid(); f = iu.next(f) {
sf := iu.srcFunc(f)
switch sf.funcID {
case abi.FuncIDWrapper:
continue
case abi.FuncID_gopanic:
if u.frame.sp == uintptr(p.startSP) && nonWrapperFrames > 0 {
canRecover = true
}
break loop
default:
nonWrapperFrames++
if nonWrapperFrames > 1 {
break loop
}
}
}
}
})
if !canRecover {
return nil
}
return p
}So recover inside a helper called by your deferred function does nothing,
and defer recover() does nothing either. It has to be called directly by the
deferred function.
panic(nil) and runtime.Goexit
Two special cases deserve their own mention, because both changed in recent Go versions.
panic(nil) used to be indistinguishable from “no panic” after a recover.
Since Go 1.21 it produces a *runtime.PanicNilError:
// src/runtime/panic.go:779
// A PanicNilError happens when code calls panic(nil).
//
// Before Go 1.21, programs that called panic(nil) observed recover returning nil.
// Starting in Go 1.21, programs that call panic(nil) observe recover returning a *PanicNilError.
// Programs can change back to the old behavior by setting GODEBUG=panicnil=1.
type PanicNilError struct {
// This field makes PanicNilError structurally different from
// any other struct in this package, and the _ makes it different
// from any struct in other packages too.
// This avoids any accidental conversions being possible
// between this struct and some other struct sharing the same fields,
// like happened in go.dev/issue/56603.
_ [0]*PanicNilError
}
func (*PanicNilError) Error() string { return "runtime error: panic called with nil argument" }
func (*PanicNilError) RuntimeError() {}runtime.Goexit is a panic that is not a panic. It runs all the deferred
calls and then kills the goroutine, and recover inside those deferred calls
returns nil:
// src/runtime/panic.go:672
// Goexit terminates the goroutine that calls it. No other goroutine is affected.
// Goexit runs all deferred calls before terminating the goroutine. Because Goexit
// is not a panic, any recover calls in those deferred functions will return nil.
//
// Calling Goexit from the main goroutine terminates that goroutine
// without func main returning. Since func main has not returned,
// the program continues execution of other goroutines.
// If all other goroutines exit, the program crashes.
//
// It crashes if called from a thread not created by the Go runtime.
func Goexit() {
// Create a panic object for Goexit, so we can recognize when it might be
// bypassed by a recover().
var p _panic
p.goexit = true
p.start(sys.GetCallerPC(), unsafe.Pointer(sys.GetCallerSP()))
for {
fn, ok := p.nextDefer()
if !ok {
break
}
fn()
}
goexit1()
}Structurally it is the same loop as gopanic. The difference is the goexit
flag, which recoverablePanic checks and refuses to recover.
Trade-offs
What open-coding buys: a defer in a normal function is a store, an OR, and
a bit test. There is no allocation, no lock, no call into the runtime, and no
GC-visible pointer in most cases.
What it costs: every exit site of the function grows. The compiler has to
emit the bit-test-and-call sequence at every return, which is why it gives
up when a function has many returns (NumReturns*NumDefers > 15) and why the
mask is limited to eight defers.
What the chain-based kinds buy: they handle the cases open-coding cannot – unbounded numbers of defers, defers in loops, defers whose arguments escape. The chain is a simple intrusive linked list, and the runtime walks it.
What they cost: deferprocStack and deferproc are real function calls into
the runtime, deferproc may allocate, and every deferred call is reached
through a pointer chase. A loop with a million defers inside it will hold a
million records alive at once.
The panic machinery pays a similar tax. Unwinding a frame that used open-coded defers requires reading funcdata, decoding two varints, and computing two stack addresses. It is not free, but it is a lot cheaper than the old scheme, where a panic had to walk the whole stack looking for defer records.
Caveats
Open-coding is an optimisation, not a guarantee. The compiler changes its mind based on the whole function: eight defers, loops, escaping arguments, race instrumentation, heap-allocated results. Never write code that depends on which kind you got.
The bitmask is one byte. Nine defers in a function disables open-coding for the whole function, not just the ninth defer.
A defer in a loop accumulates. Each iteration adds a record; none are
removed until the function returns. Ten thousand iterations means ten
thousand live records, and on the heap-allocated path that is ten thousand
allocations.
recover is frame-sensitive. It only works when called directly by a
deferred function. A recover in a helper function does nothing at all, and
it returns nil rather than panicking, so it fails silently.
recover returns the panic value, not the stack. The stack is printed by
the runtime at crash time, not captured by recover.
panic(nil) is not nil. Since Go 1.21 it is a *runtime.PanicNilError.
Code that checks if r := recover(); r != nil now behaves differently from
Go 1.20 and earlier, which is exactly why the change was made.
runtime.Goexit is not recoverable. Deferred calls run, recover returns
nil, and the goroutine dies. This is how testing stops a test function:
t.FailNow calls runtime.Goexit so that the deferred cleanup in the test
runs before the goroutine disappears.
A panic while holding a lock is fatal. gopanic checks gp.m.locks != 0
and throws rather than trying to unwind, because running deferred code with a
held lock could deadlock.
Wrapping up
The one thing to remember: a defer is a bit in a byte on the stack
when the compiler can prove it can be, and a linked-list record otherwise.
The bit is set when the defer statement runs; the exit code of the function
tests the bits and calls the matching slots.
The part people get wrong: a defer inside a loop is not a cheaper
defer, it is a different and much more expensive defer, and all of them
stay live until the function returns. The escape analyser is what decides, and
it uses loop depth, not your intent.
What to go do about it: run go build -gcflags=-m and look for
“deferring” escape notes, or benchmark your function with and without the
defer in the loop. If you need cleanup per iteration, call it explicitly at
the bottom of the loop body instead of deferring it.
Further reading
src/runtime/panic.go— the whole story:deferproc,deferprocStack,deferreturn,nextDefer,nextFrame,initOpenCodedDefers,gopanic,gorecover,recoverablePanic,Goexit,PanicNilError.src/runtime/runtime2.go— the_deferand_panicstructures, the per-P defer pool fields ong, andsavedOpenDeferState.src/runtime/coro.go— the coroutine machinery, anddeferrangefuncinsrc/runtime/panic.gofor defers inside range-over-func bodies.src/cmd/compile/internal/walk/walk.go—maxOpenDefers, the limit of eight.src/cmd/compile/internal/walk/stmt.go— where the compiler decides to give up on open-coding.src/cmd/compile/internal/escape/call.go—goDeferStmt, and theloopDepthtest that condemns defers in loops.src/cmd/compile/internal/ssagen/ssa.go—openDeferRecord,openDeferExit,emitOpenDeferInfo,deferstruct, and the call-site choice between open-coded, stack, and heap defers.src/cmd/link/internal/ld/pcln.go—computeDeferReturn, which records where each function’sdeferreturntrampoline lives.src/runtime/runtime_test.go—BenchmarkDefer,BenchmarkDefer10, andBenchmarkDeferMany.