The idea in one minute#
Functions are values: they can be stored, passed and returned, and a function literal can
capture variables from around it (a closure). Functions can return several values, and by
convention the last one is an error.
Go has no exceptions for ordinary failure. An error is a value you return and check.
defer schedules a call to run when the surrounding function returns — the mechanism for
cleanup. panic exists for bugs and impossible states, not for expected failures.
An analogy#
A relay race. Each runner hands the baton to the next and says either “all good” or “I dropped
it at the second bend”. Nobody throws the baton into the crowd and hopes someone further up the
track catches it. And every runner, whatever happened, walks off the track when their leg is
over — that is defer.
A picture#
flowchart TB
CALL["load(path)"] --> OPEN["f, err := os.Open(path)"]
OPEN --> E1{"err != nil?"}
E1 -->|"yes"| WRAP["return fmt.Errorf('load %s: %w', path, err)"]
E1 -->|"no"| DEF["defer f.Close()<br/>registered, not run yet"]
DEF --> WORK["read, parse"]
WORK --> E2{"err != nil?"}
E2 -->|"yes"| WRAP
E2 -->|"no"| RET["return result, nil"]
WRAP --> RUN["deferred calls run<br/>last in, first out"]
RET --> RUN
RUN --> CALLER["caller checks err<br/>errors.Is / errors.As"]
class CALL,CALLER neutral
class OPEN,WORK,RET compute
class E1,E2 queue
class WRAP warn
class DEF,RUN memoryHow it really works#
Functions#
func divide(a, b float64) (float64, error) { // multiple results
if b == 0 {
return 0, errors.New("divide by zero")
}
return a / b, nil
}
func sum(xs ...int) int { /* ... */ } // variadic: xs is a []int
apply := func(f func(int) int, v int) int { return f(v) } // functions are valuesA closure captures variables by reference, not by value: the function literal and the enclosing function share the same variable.
Since Go 1.22, each iteration of a for loop has its own copy of the loop variable, so
closures and goroutines started in a loop capture what you expect. Before 1.22 all iterations
shared one variable — the single most common Go bug of its first decade. for i := range 10
(range over an integer) arrived in the same release.
Errors are values#
error is an ordinary interface:
type error interface { Error() string }| Tool | Use |
|---|---|
errors.New("msg"), fmt.Errorf("...") | Create an error |
fmt.Errorf("read config: %w", err) | Wrap: add context, keep the original reachable |
errors.Is(err, fs.ErrNotExist) | Is this error, or anything it wraps, that specific value? |
errors.As(err, &target) | Is there an error of this type in the chain? Extract it |
errors.Join(e1, e2) | Combine several errors |
A sentinel: var ErrNotFound = errors.New("not found") | A value callers compare against |
| A custom type with fields | Carries data (which key, which status code) |
Rules that keep error handling readable:
- Handle an error once. Either return it (with context) or deal with it (log, retry, fall back). Doing both produces duplicate log lines.
- Add context on the way up:
fmt.Errorf("load model %q: %w", name, err). The final message reads like a stack of what was being attempted. - Compare with
errors.Is/errors.As, never==or string matching, because errors get wrapped.
Go 1.26 added errors.AsType, a generic form that needs no pre-declared target:
if pe, ok := errors.AsType[*fs.PathError](err); ok { // Go 1.26+
fmt.Println("failed path:", pe.Path)
}Defer#
defer f(x) evaluates f and x now and runs the call when the surrounding function
returns — by any path, including a panic. Deferred calls run last-in, first-out.
mu.Lock()
defer mu.Unlock() // paired where you can see both
f, err := os.Open(name)
if err != nil { return err }
defer f.Close() // only after the error check: f is nil on failureA deferred closure can read and modify named results, which is how a function adds context to, or recovers from, its own failure. Defer is cheap — a few nanoseconds in modern Go — so use it for correctness and reach for manual cleanup only in a measured hot loop.
Panic and recover#
panic unwinds the stack, running deferred calls, and crashes the program unless a deferred
function calls recover. Use it for programmer errors (an impossible state, an index out of
range). A server typically recovers once at the top of each request handler so one bad request
cannot take the process down, logs the stack, and returns a 500. Do not use panic as a
substitute for returning errors across package boundaries.
Code#
// errors.go — wrapping, inspecting, defer order, and recovering from a panic.
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
var ErrEmpty = errors.New("empty input") // a sentinel
// ParseError is an error type that carries data.
type ParseError struct {
Line int
Msg string
}
func (e *ParseError) Error() string { return fmt.Sprintf("line %d: %s", e.Line, e.Msg) }
func parse(lines []string) error {
if len(lines) == 0 {
return ErrEmpty
}
for i, l := range lines {
if l == "" {
return &ParseError{Line: i + 1, Msg: "blank line"}
}
}
return nil
}
func load(name string, lines []string) (err error) {
// A deferred closure sees and can change the named result.
defer func() {
if err != nil {
err = fmt.Errorf("load %q: %w", name, err)
}
}()
if _, statErr := os.Stat(name); statErr != nil {
return statErr
}
return parse(lines)
}
func safely(f func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered: %v", r)
}
}()
f()
return nil
}
func main() {
self, _ := os.Executable()
for _, c := range []struct {
name string
lines []string
}{
{"/no/such/file", []string{"a"}},
{self, nil},
{self, []string{"a", "", "c"}},
{self, []string{"a", "b"}},
} {
err := load(c.name, c.lines)
var pe *ParseError
switch {
case err == nil:
fmt.Println("ok")
case errors.Is(err, fs.ErrNotExist):
fmt.Println("missing file →", err)
case errors.Is(err, ErrEmpty):
fmt.Println("sentinel →", err)
case errors.As(err, &pe):
fmt.Println("typed, line", pe.Line, "→", err)
}
}
fmt.Print("\ndefer order: ")
func() {
for i := 1; i <= 3; i++ {
defer fmt.Print(i, " ") // arguments are evaluated now, calls run in reverse
}
}()
fmt.Println()
var xs []int
fmt.Println("panic →", safely(func() { _ = xs[5] }))
// Since Go 1.22 each iteration has its own i.
var fns []func() int
for i := 0; i < 3; i++ {
fns = append(fns, func() int { return i })
}
fmt.Print("closures see: ")
for _, f := range fns {
fmt.Print(f(), " ")
}
fmt.Println()
}Remember this#
- Functions return errors as ordinary values; the caller checks them.
- Wrap with
%wto add context; inspect witherrors.Isanderrors.As. deferruns at function return, in reverse order, even during a panic.panicis for bugs. Recover at a boundary, not everywhere.
Try it#
- Run
errors.go. Change%wto%vinload. Which cases stop matching, and why? - Write a function that opens two files and closes both correctly on every path.
- Set
go 1.21in ago.modand run the closure loop. What does it print?
Check yourself#
- What is the difference between
errors.Isanderrors.As? - When are a deferred call’s arguments evaluated?
- Why should an error be either returned or handled, but not both?