Go

What are the performance costs of errors, defer and panic/recover in hot paths?

Question 330HardGo 1.22 to 1.25
  • Sentinel errors are created once at package init. Returning one is just copying an interface value, with no allocation.
  • fmt.Errorf formats a string and allocates the wrapper (plus the string) on every call. In a tight loop where errors are frequent (a parser, a cache miss path), prefer a sentinel or a small struct error, and build the message lazily in Error().
  • Boxing: storing a non-pointer struct in an error interface usually allocates. A pointer type avoids the extra copy, although the &T{} itself may escape to the heap.
  • errors.Is/As walk the chain and may use reflection-like type checks (As uses reflectlite). This is cheap but not free. Deep chains or hot comparisons cost more than a plain ==.
  • defer: since Go 1.14 most defers are open-coded (inlined at the function exit) and cost about as much as a normal call. That applies when the function has at most 8 defers and none of them is in a loop. A defer in a loop falls back to the slower heap/stack-record path.
  • panic/recover is far slower than returning an error: it unwinds the stack, runs deferred calls through the runtime, and a function that contains recover handling is harder to optimize. Never use it for control flow in the normal path.
var errMiss = errors.New("cache miss") // no allocation per return

func (c *Cache) Get(k string) (V, error) {
	if v, ok := c.m[k]; ok {
		return v, nil
	}
	return V{}, errMiss // cheap; callers use errors.Is / ==
	// return V{}, fmt.Errorf("miss %q", k) // allocates on every miss
}

Measure before optimizing: go test -bench . -benchmem shows allocations per op, and go build -gcflags=-m shows what escapes. In most code the clarity of wrapped errors is worth the cost. Optimize only the paths where profiling shows it matters.

More on Error Handling & panics

All 37 Error Handling & panics questions