Go has both ordinary failure values and panic, which guarantees that programmers will eventually use one where the other belongs. I started by treating panic like an exception mechanism for file failures. That made simple callers mysterious and was the wrong model.

An operation expected to fail should return an os.Error in this snapshot:

func load(name string) ([]byte, os.Error) {
    f, err := os.Open(name, os.O_RDONLY, 0)
    if err != nil {
        return nil, err
    }
    defer f.Close()
    return readAll(f)
}

Missing files, refused connections, and malformed input are normal outcomes at a system boundary. The caller needs to decide what to do, so the value belongs in the function’s contract.

Panic is better reserved for a broken invariant or a condition from which the current operation cannot sensibly continue. During panic, normal execution stops and deferred calls run while the stack unwinds. If nothing recovers, the program terminates with diagnostic information.

recover can intercept that process, but only when called from a deferred function during unwinding. Calling it during ordinary execution does nothing useful. My worker boundary uses the pattern to prevent one internal invariant failure from taking down the entire test harness:

func runJob(job Job, result chan<- Result) {
    defer func() {
        if value := recover(); value != nil {
            result <- failedResult(value)
        }
    }()
    result <- execute(job)
}

This boundary records the failure and marks that job bad. It does not silently resume halfway through execute; its state may be inconsistent. Recovery belongs at a boundary that can abandon the failed unit of work.

I avoid broad recovery in library functions. Converting every panic into os.Error can hide programmer defects, and recovering without reporting the value destroys the most useful evidence. A panic due to an indexing mistake is not suddenly a network timeout because both traveled through one handler.

These names and details reflect the April 2011 toolchain. Panic and recovery semantics have changed during Go’s development, so old examples need their snapshot attached. The design rule is stable enough for me: return ordinary failures, panic on violated assumptions sparingly, and recover only where an entire operation can be discarded cleanly. Anything broader starts to resemble sweeping broken glass under a very concurrent rug.