The Must Pattern & No-GC Handling
Advanced Error Patterns: The “Must” & The “Odin” Way
Go’s error handling (if err != nil) is explicit, but sometimes you need different strategies for initialization vs. runtime, or you want to adopt patterns from systems languages like Odin/Zig.
1. The “Must” Pattern
Rule: Return errors to callers; Panic only on programmer error or unrecoverable startup failure.
The “Must” pattern is a convention for functions that wrap a standard error-returning function but panic instead of returning the error.
When to use it?
- Global/Package Initialization:
var t = template.Must(template.New(...)) - Startup Configuration: If your compiled regex is invalid, the app is broken. Don’t start.
Implementation
func Must[T any](obj T, err error) T {
if err != nil {
panic(err)
}
return obj
}
// Usage
var db = Must(sql.Open("postgres", "..."))
var regex = Must(regexp.Compile("^[a-z]+$"))With Generics (Go 1.18+), you can write a universal Must wrapper one time and use it everywhere.
2. Linear Error Handling (The Odin/Zig Influence)
Languages like Odin (a data-oriented C alternative) handle errors by treating them as distinct return values, much like Go, but often with syntactic sugar (or_return) or strict definitions.
While Go doesn’t have try or ? operators, we can adopt the “Guard Clause” mentality.
The Happy Path must remain left-aligned
Bad (Nested):
func process() error {
err := step1()
if err == nil {
err = step2()
if err == nil {
return step3()
}
}
return err
}Good (Guard Clauses / “Odin Style”):
func process() error {
if err := step1(); err != nil {
return fmt.Errorf("step1: %w", err)
}
// The "Happy Path" stays at indentation 0
if err := step2(); err != nil {
return fmt.Errorf("step2: %w", err)
}
return step3()
}No-GC Thinking: Errors as Resource Cleanup Triggers
In non-GC languages, an error often implies manual resource cleanup (defer in Swift/Zig). Go automates memory, but not resources (Files, Sockets, DB Connections).
The defer Trap in Loops:
for _, file := range files {
f, err := os.Open(file)
if err != nil { return err }
defer f.Close() // DANGEROUS: Closes only at end of function, not loop!
// FD exhaustion possible.
}The Fix (Anonymous Function):
for _, file := range files {
err := func() error {
f, err := os.Open(file)
if err != nil { return err }
defer f.Close() // Closes at end of this anonymous func
return process(f)
}()
if err != nil { return err }
}3. Errors in 2026: “Join” and Structure
Since Go 1.20+, errors.Join allows returning multiple errors at once (e.g., from parallel validation).
func validate(u User) error {
var errs error
if u.Name == "" {
errs = errors.Join(errs, errors.New("missing name"))
}
if u.Age < 0 {
errs = errors.Join(errs, errors.New("invalid age"))
}
return errs // Returns nil if no errors joined
}This is cleaner than older multierror libraries.
Summary
- Must: Use for hard startup dependencies. Crash early.
- Left-Align: Keep your happy path on the left edge.
- Defer scope: Remember
deferis function-scoped, not block-scoped.