Go Cheatsheet

Error Handling

Use this Go reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

The error Interface

error is a built-in interface with one method.

type error interface {
    Error() string
}

Return nil for success, a non-nil error on failure. Always handle errors — Go has no exceptions.

Returning Errors

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

result, err := divide(10, 0)
if err != nil {
    log.Fatal(err)
}
fmt.Println(result)

errors Package

import "errors"

// Create a simple error
err := errors.New("something went wrong")

// Wrap an error (adds context)
wrapped := fmt.Errorf("open config: %w", err)

// Unwrap — get the inner error
inner := errors.Unwrap(wrapped)

// Is — check if any error in the chain matches a target
errors.Is(wrapped, err)         // true
errors.Is(err, fs.ErrNotExist)  // check standard errors

// As — extract a specific type from the chain
var pe *os.PathError
if errors.As(err, &pe) {
    fmt.Println("path:", pe.Path)
}

fmt.Errorf with %w

%w wraps the original error so it is reachable via errors.Is/errors.As.

func readConfig(path string) error {
    data, err := os.ReadFile(path)
    if err != nil {
        return fmt.Errorf("readConfig %s: %w", path, err)
    }
    _ = data
    return nil
}

err := readConfig("/missing.json")
errors.Is(err, os.ErrNotExist)   // true — walks the chain

%v embeds the message but does NOT wrap (chain is broken); %w wraps.

Sentinel Errors

Package-level error variables that callers can compare.

var (
    ErrNotFound   = errors.New("not found")
    ErrPermission = errors.New("permission denied")
    ErrTimeout    = errors.New("operation timed out")
)

func lookup(id int) (User, error) {
    if id <= 0 {
        return User{}, ErrNotFound
    }
    // ...
}

u, err := lookup(-1)
if errors.Is(err, ErrNotFound) {
    // handle missing
}

Custom Error Types

Implement the error interface on your own struct for structured errors.

type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message)
}

func validate(name string) error {
    if name == "" {
        return &ValidationError{Field: "name", Message: "must not be empty"}
    }
    return nil
}

err := validate("")
var ve *ValidationError
if errors.As(err, &ve) {
    fmt.Println(ve.Field, ve.Message)
}

Wrapping Multiple Errors (Go 1.20+)

errors.Join combines multiple errors into one. Each is reachable via errors.Is/errors.As.

import "errors"

err1 := errors.New("db failed")
err2 := errors.New("cache failed")

combined := errors.Join(err1, err2)
fmt.Println(combined)           // db failed\ncache failed
errors.Is(combined, err1)       // true
errors.Is(combined, err2)       // true

// fmt.Errorf with multiple %w (Go 1.20+)
err := fmt.Errorf("two failures: %w and %w", err1, err2)
errors.Is(err, err1)  // true

Panic and Recover

panic is not for normal errors. Use it for unrecoverable states or invariant violations.

func mustPositive(n int) int {
    if n <= 0 {
        panic(fmt.Sprintf("expected positive, got %d", n))
    }
    return n
}

// Recover in a deferred function
func safeRun(f func()) (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered panic: %v", r)
        }
    }()
    f()
    return nil
}

err := safeRun(func() { mustPositive(-1) })
fmt.Println(err)  // recovered panic: expected positive, got -1

Printing a stack trace inside recover

import "runtime/debug"

defer func() {
    if r := recover(); r != nil {
        fmt.Fprintf(os.Stderr, "panic: %v\n%s\n", r, debug.Stack())
    }
}()

log Package (for Fatal errors)

import "log"

log.Print("message")
log.Printf("value: %v", v)
log.Println("done")
log.Fatal("fatal: exits with code 1")   // calls os.Exit(1) after logging
log.Fatalf("fatal: %v", err)
log.Panic("panic: then panics")          // logs then calls panic()

Structured logging (Go 1.21+ slog)

import "log/slog"

slog.Info("user created", "id", 42, "name", "Alice")
slog.Error("request failed", "err", err, "url", url)
slog.Warn("quota exceeded", "used", 900, "limit", 1000)
slog.Debug("cache miss", "key", key)

// JSON handler
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("started", "addr", ":8080")
logger.With("requestID", "abc123").Info("handling request")

Idiomatic Error Handling Patterns

Early return

func process(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return fmt.Errorf("open: %w", err)
    }
    defer f.Close()

    data, err := io.ReadAll(f)
    if err != nil {
        return fmt.Errorf("read: %w", err)
    }

    if err := parse(data); err != nil {
        return fmt.Errorf("parse: %w", err)
    }

    return nil
}

errgroup (parallel error collection)

import "golang.org/x/sync/errgroup"

g, ctx := errgroup.WithContext(context.Background())

g.Go(func() error {
    return fetchUser(ctx, id)
})
g.Go(func() error {
    return fetchOrders(ctx, id)
})

if err := g.Wait(); err != nil {
    return err  // first non-nil error
}

Inline error checking helper

// For scripts or CLI tools where every error is fatal
func must[T any](v T, err error) T {
    if err != nil { panic(err) }
    return v
}

data := must(os.ReadFile("config.json"))

Standard Error Variables

SentinelPackageMeaning
io.EOFioend of file (expected)
io.ErrUnexpectedEOFioEOF in middle of data
io.ErrClosedPipeiowrite to closed pipe
os.ErrNotExistosfile not found
os.ErrExistosfile already exists
os.ErrPermissionospermission denied
os.ErrDeadlineExceededosI/O deadline exceeded
context.Canceledcontextcontext was canceled
context.DeadlineExceededcontextcontext deadline exceeded
sql.ErrNoRowsdatabase/sqlquery returned no rows
http.ErrServerClosednet/httpserver shutdown gracefully

Gotchas

// 1. Typed nil interface is not a nil error
func badFunc() error {
    var p *MyError = nil
    return p     // returns non-nil error! Interface wraps (type=*MyError, value=nil)
}
// Fix: return nil explicitly

// 2. errors.Is uses == for comparisons; for custom types, implement Is()
func (e *MyError) Is(target error) bool {
    t, ok := target.(*MyError)
    return ok && t.Code == e.Code
}

// 3. errors.As requires a pointer-to-pointer
var ve *ValidationError
errors.As(err, &ve)  // correct
// errors.As(err, ve) // wrong: not a pointer to the concrete type

// 4. Don't ignore errors
os.Remove("tmp")         // WRONG: ignoring error
_ = os.Remove("tmp")     // explicit ignore (acceptable when truly fine)
if err := os.Remove("tmp"); err != nil { ... }  // best

// 5. Add context at each layer; avoid repeating error text
// Bad:  fmt.Errorf("failed to read file: %w", err)  where err already says "no such file"
// Good: fmt.Errorf("load config %s: %w", path, err)