agentsclimarketplace

Go defer panic recover

Skill ctoth/golang-skills-plugin/plugins/golang/skills/go-defer-panic-recover

Agent skill plugins for Go code quality and performance work.

Install
npx -y skills add ctoth/golang-skills-plugin --skill go-defer-panic-recover

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Guides Go's defer, panic, and recover — the LIFO ordering and arguments-evaluated-at-the-defer-statement semantics, why defer is near-free since open-coded defers (so don't avoid it except in tight loops), the loop-defer handle-leak pitfall, panic only for programmer bugs and unrecoverable states (not ordinary failure), and recover only inside a deferred function at a boundary to convert a panic into an error. Auto-invokes when writing or editing defer, panic, recover, or deferred cleanup, and on "why does this defer run in the wrong order", "should this panic", or "how do I stop a panic from crashing the process". Routes ordinary error-as-value handling to go-error-handling.

SKILL.md

12.0 KB, as published. Nobody here has run it

Go Defer, Panic, and Recover

"Deferred function calls are executed in Last In First Out order after the surrounding function returns." · "A deferred function's arguments are evaluated when the defer statement is evaluated." — Defer, Panic, and Recover (Go blog)

"Don't use panic for normal error handling. Use error and multiple return values." — Go Code Review Comments — Don't Panic

defer, panic, and recover are three small mechanisms with sharp edges. defer is the everyday one — get its evaluation timing and loop behavior right. panic and recover are the rare ones — used for ordinary failure they turn Go into a worse exception language. This skill owns the mechanics and policy; ordinary errors-as-values route to go-error-handling.


1. What defer Actually Does

Four facts, each load-bearing:

  1. LIFO at return. "Deferred function calls are executed in Last In First Out order after the surrounding function returns" (Go blog). The last defer runs first.
  2. Arguments evaluated at the defer statement. "A deferred function's arguments are evaluated when the defer statement is evaluated" (Go blog) — not when the call later runs. This is the classic gotcha (Section 3).
  3. Runs even on panic. "When the function F calls panic, execution of F stops, any deferred functions in F are executed normally, and then F returns to its caller" (Go blog). This is why defer is the right place for cleanup — it fires on every return path, including a panic.
  4. Can modify named return values. "Deferred functions may read and assign to the returning function's named return values" (Go blog). The mechanism behind recover-to-error (Section 6) and Close-error capture (owned by go-error-handling).

The everyday payoff: "Deferring a call to Close ... guarantees you will never forget to close the file ... [and] the close sits near the open, which is much clearer than placing it at the end" (Effective Go).


2. The Rules

RuleThe disciplineSource
defer is for cleanupPut release next to acquire; it runs on every return path"an effective way to handle resource cleanup regardless of which return path is taken" (Effective Go)
Args bind at the defer lineDon't expect a deferred arg to read a later value"arguments are evaluated when the defer statement is evaluated" (Go blog)
Don't defer in a loop for per-iteration cleanupClose in the loop body or extract a functionLoop-defer accumulates until the function returns (Section 4)
defer is not a perf concernNear-zero overhead since Go 1.14 open-coded defers"improves the performance of most uses of defer to incur almost zero overhead" (Go 1.14)
panic is for bugs, not failureOrdinary failures return an error"Don't use panic for normal error handling" (CodeReviewComments)
recover only in a deferred funcAnywhere else it returns nil and does nothing"Recover is only useful inside deferred functions" (Go blog)
Recover only at a boundaryConvert a panic to an error; re-panic what you can't handle"regains control of a panicking goroutine" (Go blog)

3. Arguments Evaluate at the defer Statement

The single most common defer surprise: the deferred call's arguments are snapshotted when defer runs, even though the call runs at return.

// WRONG — expecting start to be re-read at return; it is captured as the start value
func timed() {
	start := time.Now()
	defer log.Printf("took %v", time.Since(start)) // time.Since(start) runs NOW, logging ~0
	doWork()
}

// RIGHT — defer a closure so the expression runs at return time
func timed() {
	start := time.Now()
	defer func() { log.Printf("took %v", time.Since(start)) }()
	doWork()
}

The argument form evaluates time.Since(start) immediately; the closure form defers the whole expression. The same fact makes LIFO observable: for i := 0; i < 5; i++ { defer fmt.Printf("%d ", i) } "Prints: 4 3 2 1 0" (Effective Go) — each i is captured at its iteration, then the calls run in reverse.


4. The Loop-Defer Pitfall

defer fires at function return, not at the end of the loop body. Deferring Close inside a for accumulates open handles for the whole function — a file-descriptor leak that only shows up under load.

// WRONG — every file stays open until processAll returns; thousands of names => fd exhaustion
func processAll(names []string) error {
	for _, name := range names {
		f, err := os.Open(name)
		if err != nil {
			return err
		}
		defer f.Close() // does NOT close at end of iteration
		use(f)
	}
	return nil
}

// RIGHT — extract the body so defer fires per item
func processAll(names []string) error {
	for _, name := range names {
		if err := processOne(name); err != nil {
			return err
		}
	}
	return nil
}

func processOne(name string) error {
	f, err := os.Open(name)
	if err != nil {
		return err
	}
	defer f.Close() // fires at processOne's return — once per item
	use(f)
	return nil
}

Note the inverse: in a non-loop function, don't avoid defer for performance. Open-coded defers (Go 1.14) made it "incur almost zero overhead" (Go 1.14); Uber's guide agrees it "has an extremely small overhead and should be avoided only if you can prove that your function execution time is in the order of nanoseconds" (Uber Go Style Guide).


5. panic Is for Programmer Errors, Not Ordinary Failure

panic unwinds the stack and, unrecovered, crashes the process. Reserve it for bugs and truly unrecoverable states: a violated invariant, an impossible switch case, a nil that the code's own contract forbids. Ordinary, foreseeable failure — bad input, a missing file, a failed lookup — is an error the caller decides about. "Don't use panic for normal error handling. Use error and multiple return values" (CodeReviewComments); the Google style guide repeats it: "Do not use panic for normal error handling. Instead, use error and multiple return values" (Google Go Style Guide).

Legitimate uses are narrow: an init that cannot establish a required invariant — "panic is reasonable during initialization if the library cannot set itself up" (Effective Go) — or a MustCompile-style helper for compile-time-constant input. The depth of returning errors instead (wrapping, errors.Is/As, named-return decoration) is owned by go-error-handling.


6. recover Only in a Deferred Func, Only at a Boundary

"Recover is a built-in function that regains control of a panicking goroutine. Recover is only useful inside deferred functions. During normal execution, a call to recover will return nil and have no other effect" (Go blog). So recover does something only when (a) it sits in a deferred function and (b) a panic is in flight. Use it at a boundary — the top of a request handler, or a goroutine you own — to convert a panic into an error rather than crash, then re-establish invariants.

// RIGHT — a boundary converts a panic into an error via a named return
func safeInvoke(fn func()) (err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("recovered: %v", r) // re-establish: a panic is now an error
		}
	}()
	fn()
	return nil
}

// WRONG — recover outside a deferred func: returns nil, catches nothing, fn's panic still crashes
func brokenGuard(fn func()) {
	if r := recover(); r != nil { // never true here
		log.Println(r)
	}
	fn()
}

// WRONG — swallowing the panic with no log and no re-raise: the bug vanishes silently
func swallow(fn func()) {
	defer func() { recover() }()
	fn()
}

Two non-negotiables:

  • Re-panic what you can't handle. A recover that catches every value and discards it hides real bugs. Inspect the recovered value; if it is not one you can turn into a clean error, log it and panic(r) again.
  • Don't recover across goroutines. A deferred recover only catches a panic in its own goroutine. "if do(work) panics, the error is logged and the goroutine exits cleanly without killing other goroutines" (Effective Go) — but a panic in a goroutine you spawned without its own recover crashes the whole program. Wrap each goroutine you own. See go-concurrency-goroutines.

recover is not general control flow. Don't use panic/recover as exceptions to jump across ordinary call layers — that is the exception-language anti-pattern go-idiomatic-discipline bans.


7. Who Suffers When This Is Done Badly

The author never feels it at write time; someone downstream does:

  • The on-call engineer paged when a service exhausts file descriptors under load — a defer f.Close() inside a for that never closed until the long-lived function returned (Section 4).
  • The whole user base taken down when one request, or one un-wrapped worker goroutine, panics on bad input the code should have returned as an error (Sections 5–6).
  • The next debugger who spends an afternoon chasing a bug that a bare defer func(){ recover() }() silently swallowed three layers down — no log, no re-raise, no trace (Section 6).

defer/panic/recover done well is invisible; done badly it is a 3am page or a corrupted-state mystery.


8. Routing to the Specific Skills

This skill owns defer/panic/recover mechanics and policy. Adjacent depth lives elsewhere:

  • go-idiomatic-discipline — the policy root: "don't panic for ordinary failure" is one of its headline floor rules; panic-as-exceptions is one of its named anti-patterns.
  • go-error-handling — errors as values: %w wrapping, errors.Is/As, and the defer-based named-return error decoration and Close-error-capture idioms (this skill teaches the defer/named-return mechanism; that skill teaches the error idiom built on it). Return an error instead of panicking — owned there.
  • go-concurrency-goroutines — a panic in a goroutine you spawned without its own deferred recover crashes the process; wrap every goroutine you own.
  • go-context — cancellation and defer cancel(); a recover boundary often sits at the same request edge where a context is derived.

9. Reference Files

High-frequency defer/panic/recover anti-patterns in LLM-generated Go, each with wrong/right code and citations:

${CLAUDE_SKILL_DIR}/references/common-mistakes.md

Source provenance for every claim in this skill:

${CLAUDE_SKILL_DIR}/references/sources.yaml

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.