Go Receivers & Control Flow
Value vs. Pointer Receivers
What: func (t T) M() operates on a copy; func (t *T) M() on the original. Choice affects mutation, copy cost, method sets, and interface satisfaction.
Syntax/Signature: Method set rule: T → value-receiver methods only; *T → both.
type Counter struct{ n int }
func (c *Counter) Inc() { c.n++ } // mutates
func (c Counter) Value() int { return c.n } // read-only, copy is fine
var c Counter
c.Inc() // sugar: (&c).Inc() — works because c is addressable
How it works under the hood: A value receiver receives a copy in registers/stack; a pointer receiver receives an address, which typically forces the value to escape to the heap if the compiler can't prove it doesn't outlive the frame. The auto-& sugar only applies to addressable operands — map elements, values inside interfaces, and function-return temporaries are not addressable, so pointer-receiver calls on them fail to compile.
Interview angle: "Rules of thumb for choosing?" — (1) needs mutation or identity → pointer; (2) large struct → pointer; (3) contains a sync.Mutex or other no-copy field → pointer, always (see [[Go Sync Primitives]]); (4) consistency: don't mix receivers on one type; (5) map/chan/func/small immutable values → value is fine.
Gotchas / common wrong answers:
- "Value receivers are always slower for big structs" — often, but escape-analysis heap allocation from pointer receivers can cost more than a copy; measure.
- Mixing receivers, then being surprised
Tdoesn't satisfy an interface needing the pointer-receiver method (see [[Go Interface Internals]]). - Calling a pointer-receiver method in a
for rangeover a slice of values:for _, v := range xs { v.Inc() }mutates the copyv, not the element — usefor i := range xs { xs[i].Inc() }.
Trade-offs / when NOT to use: Value receivers enable safe concurrent reads without locks and keep values usable in maps. Pointer receivers are viral: once one method needs *T, make them all *T.
defer / panic / recover
What: defer schedules a call to run when the function returns (LIFO); panic unwinds the stack running defers; recover inside a deferred function stops the unwind and returns the panic value.
Syntax/Signature: defer f(args) — args evaluated at defer time; recover() is meaningful only when called directly by a deferred function.
func CopyFile(dst, src string) (err error) {
r, err := os.Open(src)
if err != nil {
return err
}
defer r.Close() // read side: error on close rarely matters
w, err := os.Create(dst)
if err != nil {
return err
}
defer func() { // write side: Close error is a data-loss signal
if cerr := w.Close(); cerr != nil && err == nil {
err = cerr // named return lets defer override
}
}()
_, err = io.Copy(w, r)
return err
}
How it works under the hood: Since 1.14, most defers are open-coded: the compiler inlines the deferred call at each return site guarded by a bitmask — near-zero overhead (~1ns), no allocation. Defers in loops or with variable counts fall back to heap/stack-allocated _defer records in a linked list. panic walks that list; recover flips a flag so unwinding stops and the function returns normally (which is why named returns are the only way to hand a value back after recovery).
Interview angle: "What does defer fmt.Println(i) print if i changes later?" — the value at defer time. "When is recover useless?" — called outside a deferred func, or in a helper the deferred func calls indirectly — recover works only when directly called by the deferred function. "Panic across goroutines?" — a panic in a goroutine cannot be recovered by its parent; it kills the process. That's why worker pools wrap each task in its own defer/recover.
Gotchas / common wrong answers:
defer resp.Body.Close()before checkingerr != nilfromhttp.Get— on errorrespmay be nil ⇒ nil-pointer panic inside defer.- Defer in a loop: runs at function end, not iteration end — file handles pile up; extract an inner function.
- Believing
recovergives you the stack — pair it withdebug.Stack()yourself, and re-panic (panic(r)) if you can't actually handle it. - Deferred
func() { err = ... }requires a named return; assigning to a local does nothing.
Trade-offs / when NOT to use: Panics are for programmer errors (impossible states), never control flow across API boundaries — libraries recover at the boundary and return errors (encoding/json does exactly this internally).
Interview Drills
- A slice of structs is looped with
for _, v := range xs { v.Inc() }whereIncis a pointer-receiver method, but the elements never change. Why? —vis a fresh copy of each element on every iteration — callingInc()on it (via the compiler's auto-&sugar, sincevitself is addressable) mutates that copy, not the original element in the slice. Fix: index directly,for i := range xs { xs[i].Inc() }, which operates on the real element's address. - Why does
defer resp.Body.Close()immediately afterresp, err := http.Get(url), before checkingerr, risk a panic? — On a genuine error,http.Getcan return a nilresp— deferringresp.Body.Close()before confirmingerr == nilschedules a call on a nil pointer's field, which panics when the deferred call actually runs. Always check the error first, defer the close only in the success path. - Why can't a goroutine's panic be recovered by the goroutine that spawned it? — Each goroutine has its own independent stack and panic/recover mechanism — a panic unwinds only its own goroutine's stack; there's no cross-goroutine propagation of panics at all. An unrecovered panic in any goroutine crashes the entire process, which is why worker pools wrap each individual task in its own
defer/recoverrather than relying on an outer recover to catch failures from spawned goroutines.
Cross-links
[[Go Interface Internals]] · [[Go Sync Primitives]] · [[Go Data Structure Internals]] · [[Go Goroutines & Scheduling]]