# Go

> Idioms for errors, concurrency and interfaces, the toolchain commands worth memorising, and the traps that survive code review.

Canonical: https://www.wiki.jodisand.me/go/
Reviewed: 2026-09-24
Related: [Rust](https://www.wiki.jodisand.me/rust/index.md), [Python](https://www.wiki.jodisand.me/python/index.md), [Testing](https://www.wiki.jodisand.me/testing/index.md), [HTTP and curl](https://www.wiki.jodisand.me/http/index.md)


Go trades expressiveness for a small language, a fast toolchain and a runtime that makes concurrency and garbage collection cheap. The idioms below are the ones that decide whether a service is correct under load: how errors carry context, how cancellation propagates, and how goroutines stop. Verify version-gated behaviour against [go.dev/doc](https://go.dev/doc/) and package APIs against [pkg.go.dev](https://pkg.go.dev/).

## Cheatsheet

| Task | Command |
| --- | --- |
| Build for Linux, static | `CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags='-s -w' ./cmd/api` |
| Run tests with race detection | `go test -race ./...` |
| One test, verbose | `go test -run TestName -v ./pkg/...` |
| Coverage report | `go test -coverprofile=c.out ./... && go tool cover -html=c.out` |
| Benchmarks with allocations | `go test -bench=. -benchmem ./...` |
| CPU profile | `go test -cpuprofile=cpu.out -bench=.` then `go tool pprof cpu.out` |
| Profile a live server | `go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30` |
| Vet and lint | `go vet ./... && golangci-lint run` |
| Tidy dependencies | `go mod tidy && go mod verify` |
| Upgrade one module | `go get example.com/pkg@v1.4.2` |
| Why is this dependency here | `go mod why -m example.com/pkg` |
| Dependency graph | `go mod graph \| grep pkg` |
| Vulnerability scan | `govulncheck ./...` |
| Escape analysis | `go build -gcflags='-m' ./... 2>&1 \| grep escapes` |
| Update to a new Go version | edit the `go` directive, then `go mod tidy` |

## Errors

An error is an ordinary value, not a thrown exception. A function returns one, the caller checks it, and each layer wraps it with context the caller could not already know: the operation and its inputs. Compare with `errors.Is` and `errors.As` rather than matching on the message string, which breaks the moment the wording changes.

```go
if err != nil {
    return fmt.Errorf("fetch user %d: %w", id, err)   // %w keeps the wrapped error in the chain
}

var pathErr *fs.PathError
if errors.As(err, &pathErr) { /* inspect pathErr.Path */ }
if errors.Is(err, context.DeadlineExceeded) { /* the request timed out */ }

// A sentinel for a condition callers must branch on
var ErrNotFound = errors.New("not found")
```

`fmt.Errorf("error: %w", err)` adds nothing but noise. Wrap with `%w` only when callers should be able to unwrap and inspect the cause; use `%v` to keep the message but hide the type, which is right for an internal error you do not want to expose in your API contract.

A deferred `Close` on a writable file can fail after every write succeeded, so capture its error instead of discarding it:

```go
defer func() {
    if cerr := f.Close(); cerr != nil && retErr == nil {
        retErr = fmt.Errorf("close: %w", cerr)
    }
}()
```

## Context

`context.Context` carries deadlines, cancellation and request-scoped values across API boundaries. It is not a bag for optional parameters. Pass it as the first argument, never store it in a struct, and never pass `nil`; use `context.Background()` at the top of a `main` or a test and derive from it.

```go
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()                                    // release resources even on the success path

req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)

select {
case <-ctx.Done():
    return ctx.Err()                              // Canceled or DeadlineExceeded
case res := <-ch:
    return res, nil
}
```

Every blocking call in a request path should take the context so it unwinds when the client goes away. A goroutine that ignores cancellation is a leak with extra steps.

## Concurrency

Goroutines are cheap to start; the coordination is what costs. Start one only when you can answer two questions: who waits for it to finish, and how does it stop. Use `errgroup` (from `golang.org/x/sync/errgroup`) when several tasks run together and the first failure should cancel the rest.

```go
g, ctx := errgroup.WithContext(ctx)
for _, id := range ids {
    g.Go(func() error {
        return process(ctx, id)                   // loop variable is per-iteration since Go 1.22
    })
}
if err := g.Wait(); err != nil { return err }     // first non-nil error cancels the group's context
```

Bound fan-out with a buffered channel so a burst of work does not spawn unbounded goroutines:

```go
sem := make(chan struct{}, 8)                     // at most 8 in flight
for _, job := range jobs {
    sem <- struct{}{}
    go func(j Job) { defer func() { <-sem }(); work(j) }(job)
}
```

| Trap | Reality |
| --- | --- |
| Unbuffered channel send with no receiver | Blocks forever; the goroutine leaks |
| `range` over a channel nobody closes | Blocks forever |
| Closing a channel from the receiver | Panic on the next send; the sender closes |
| Loop variable captured in a closure | Per-iteration since Go 1.22; earlier versions share one variable |
| `sync.WaitGroup` copied into a function | Copies the counter; pass a pointer |
| Mutex copied with its struct | `go vet` catches it; use pointer receivers |
| Reading a map from several goroutines while writing | Data race; the runtime may panic outright |

`go test -race` is not optional for concurrent code. It instruments memory access and catches the bug that reproduces once a fortnight in production.

## Interfaces and structure

Define an interface in the package that consumes it, describing what that package needs, not what some type happens to provide. An interface with one implementation and one caller usually should not exist yet. Accept interfaces so callers can substitute a fake in tests; return concrete types so callers keep every method and a usable zero value.

```go
// In the consumer package
type UserStore interface {
    Get(ctx context.Context, id int64) (*User, error)
}

func NewHandler(s UserStore) *Handler { return &Handler{store: s} }
```

A nil pointer stored in a non-nil interface is not equal to nil, which is a common source of "impossible" nil checks that never fire:

```go
var p *MyError            // a nil pointer
var err error = p         // an interface value holding a nil *MyError, so it is non-nil
fmt.Println(err == nil)   // false
```

Return `nil` for the error, not a typed nil pointer, to avoid this.

## Slices, maps and strings

A slice is a view (pointer, length, capacity) over a backing array, so two slices can share storage and a write through one is visible through the other. `append` may reallocate, so always assign its result back. Strings are immutable bytes; ranging over one yields runes, not bytes.

```go
s := make([]int, 0, 100)        // length 0, capacity 100: 100 appends without reallocating
b := s[1:3]                     // shares the backing array; a write to b changes s
c := slices.Clone(s)            // an independent copy
s = slices.Delete(s, 1, 2)      // removes index 1, shifting the tail down in place
clear(m)                        // empty a map without reallocating (Go 1.21+)

for i, r := range "héllo" {     // i is a byte offset, r is a rune
    _ = r
}
len("héllo")                    // 6 bytes
utf8.RuneCountInString("héllo") // 5 runes
```

A slice of a large array keeps the whole array alive, so `slices.Clone` when you retain a small piece of something big. For repeated concatenation, `strings.Builder` avoids the O(n²) of `+`:

```go
var b strings.Builder
for _, s := range parts { b.WriteString(s) }
result := b.String()
```

## HTTP services

The standard library is enough for a production HTTP server, but it ships with no timeouts, so a slow or hostile client can hold connections open indefinitely. Set every server timeout, and give in-flight requests a bounded window to drain on shutdown. See [HTTP timing](https://www.wiki.jodisand.me/http/#timing) for which phase to instrument when a request is slow.

```go
srv := &http.Server{
    Addr:              ":8080",
    Handler:           mux,
    ReadHeaderTimeout: 5 * time.Second,     // time to read request headers; blocks Slowloris
    ReadTimeout:       15 * time.Second,    // time to read the entire request
    WriteTimeout:      30 * time.Second,    // time to write the response
    IdleTimeout:       60 * time.Second,    // keep-alive idle period
}

go func() {
    if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
        log.Error("server", "err", err)     // ErrServerClosed is the normal shutdown signal
    }
}()

<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
_ = srv.Shutdown(shutdownCtx)               // stop accepting, drain in-flight requests
```

`http.Client` also has no default timeout, so a hung upstream hangs your service. Construct one, and always drain and close the response body or the connection is not reused.

```go
client := &http.Client{
    Timeout: 10 * time.Second,              // covers connect, redirects and reading the body
    Transport: &http.Transport{
        MaxIdleConnsPerHost: 100,
        IdleConnTimeout:     90 * time.Second,
    },
}
// per request:
defer resp.Body.Close()
io.Copy(io.Discard, resp.Body)              // drain remaining bytes so the connection can be reused
```

## Testing

Table-driven tests keep one assertion path and vary the inputs, so a new case is a new row. Run subtests in parallel where they are independent, compare structs with `go-cmp` for a readable diff, and match errors with `errors.Is`. See [testing](https://www.wiki.jodisand.me/testing/) for choosing between table tests, golden files and fuzzing.

```go
func TestParse(t *testing.T) {
    tests := []struct {
        name string
        in   string
        want Config
        err  error
    }{
        {name: "empty", in: "", err: ErrEmpty},
        {name: "valid", in: "a=1", want: Config{A: 1}},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel()
            got, err := Parse(tt.in)
            if !errors.Is(err, tt.err) {
                t.Fatalf("err = %v, want %v", err, tt.err)
            }
            if diff := cmp.Diff(tt.want, got); diff != "" {
                t.Errorf("mismatch (-want +got):\n%s", diff)
            }
        })
    }
}
```

`t.Fatalf` stops the current subtest; `t.Errorf` records the failure and continues. `t.Cleanup` runs teardown in reverse order and works inside helpers where `defer` would fire too early; `t.TempDir` makes a directory that removes itself. Since Go 1.24, `t.Context()` gives a context cancelled just before cleanup.

Benchmarks should use `for b.Loop()` (Go 1.24+), which runs the body the right number of times and stops the compiler from optimising the call away:

```go
func BenchmarkParse(b *testing.B) {
    b.ReportAllocs()
    for b.Loop() {
        _, _ = Parse(input)
    }
}
```

## Modules and builds

A module is the unit of versioning; `go.mod` declares the module path and the minimum Go version, and `go.sum` pins the checksum of every dependency. `go mod tidy` reconciles both with what the code actually imports.

```sh
go mod init example.com/api
go get example.com/pkg@latest                  # add or upgrade a dependency
go mod tidy                                     # add missing imports, remove unused ones
go mod vendor                                   # vendor deps into ./vendor for an offline build
go list -m -u all                               # show available upgrades for every module
go build -ldflags="-X main.version=$(git describe --tags)" ./cmd/api   # stamp the version
GOFLAGS=-mod=readonly go build ./...            # fail the build if go.mod would change
```

Keep executables in `cmd/<name>/`, importable packages at the module root, and code you never want other modules to import under `internal/`, which the toolchain enforces.

## Profiling

`net/http/pprof` exposes the runtime profiles over HTTP. Attach it to a debug listener bound to loopback, because it exposes memory contents and stack traces.

```go
import _ "net/http/pprof"                        // registers handlers on the default mux
go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }()
```

```sh
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/heap        # heap, interactive
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30      # 30s CPU profile
curl -o trace.out 'http://localhost:6060/debug/pprof/trace?seconds=5' && go tool trace trace.out
GODEBUG=gctrace=1 ./api                          # GC pauses and heap growth on stderr, no code change
```

## Generics

Type parameters (Go 1.18+) let one function or type work over several types while staying statically checked. A constraint is an interface that lists the allowed types or methods; `any` allows everything, `comparable` allows `==`, and `golang.org/x/exp/constraints` and the standard `cmp` package supply `Ordered`. Reach for generics when the alternative is copy-pasting a function per type or losing type safety through `interface{}`; do not reach for them to make an abstraction "flexible" before a second concrete type exists.

```go
func Map[T, U any](in []T, f func(T) U) []U {
    out := make([]U, 0, len(in))
    for _, v := range in { out = append(out, f(v)) }
    return out
}

func Max[T cmp.Ordered](a, b T) T {            // cmp.Ordered: integers, floats, strings
    if a > b { return a }
    return b
}

type Number interface{ ~int | ~int64 | ~float64 }   // ~ admits named types whose underlying type matches

type Cache[K comparable, V any] struct {       // a generic type; methods cannot add their own type parameters
    mu sync.Mutex
    m  map[K]V
}
func (c *Cache[K, V]) Get(k K) (V, bool) { c.mu.Lock(); defer c.mu.Unlock(); v, ok := c.m[k]; return v, ok }
```

Since Go 1.23 a function with signature `func(yield func(K, V) bool)` is a range-over-func iterator, and the `iter`, `slices` and `maps` packages build on it: `for k, v := range maps.All(m)`, `slices.Sorted(maps.Keys(m))`, `slices.Collect(seq)`. Type inference covers most calls, so `Map(ids, strconv.Itoa)` needs no explicit instantiation; when it fails the error names the parameter it could not infer, and `Map[int, string](...)` fixes it.

## Goroutine patterns

Three shapes cover most concurrent code. A worker pool takes jobs from one channel and writes results to another, with a `WaitGroup` closing the results channel once every worker has returned. A pipeline chains stages by channel, each stage closing its output when its input closes. A fan-in merges several channels into one. In every shape the sender closes, the receiver ranges, and every blocking operation also selects on `ctx.Done()` so a cancelled request does not leave goroutines parked forever.

```go
func pool(ctx context.Context, jobs <-chan Job, workers int) <-chan Result {
    results := make(chan Result)
    var wg sync.WaitGroup
    for i := 0; i < workers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for j := range jobs {                       // exits when jobs is closed
                select {
                case results <- work(ctx, j):
                case <-ctx.Done():
                    return
                }
            }
        }()
    }
    go func() { wg.Wait(); close(results) }()           // exactly one closer, after all senders finish
    return results
}
```

`sync.Once` initialises shared state exactly once; `sync.OnceValue` (Go 1.21+) wraps a function so its first result is memoised. `singleflight.Group` (in `golang.org/x/sync`) collapses concurrent identical requests, such as a cache miss storm, into one upstream call. Prefer a mutex around a plain map to `sync.Map`, which is tuned for append-only caches with disjoint key sets. `time.After` in a `select` inside a loop allocates a timer per iteration; use `time.NewTimer` and `Reset`, or a `time.Ticker`, when the loop is hot.

## go test flags

```sh
go test ./...                                    # every package; results are cached per package
go test -count=1 ./...                           # bypass the test cache
go test -run 'TestParse/valid' ./pkg             # regex on Test name, then on subtest name, separated by /
go test -skip 'TestSlow' ./...                   # Go 1.20+: inverse of -run
go test -short ./...                             # tests call testing.Short() to skip long paths
go test -timeout 2m ./...                        # panic with a goroutine dump when the package exceeds it (default 10m)
go test -shuffle=on ./...                        # randomise test order to expose ordering dependencies
go test -failfast ./...                          # stop after the first failing test
go test -v -json ./... | go tool test2json        # machine-readable events for CI dashboards
go test -fuzz=FuzzParse -fuzztime=30s ./pkg      # run a fuzz target; failing inputs land in testdata/fuzz/
go test -cover -coverpkg=./... ./...             # coverage of all packages, not just the one under test
go test -bench=. -benchtime=5s -count=6 ./pkg | tee new.txt && benchstat old.txt new.txt   # statistically compare
```

`-race`, `-cover` and `-count` invalidate the cache, so a CI job that combines them is never served stale results. `TESTFLAGS` does not exist; put shared flags in `GOFLAGS` (`GOFLAGS=-race -shuffle=on`) or a Makefile target.

## Workspaces, build tags and embed

`go work` lets several modules build against each other's working copies without `replace` directives, which is how you test a library change against the service that consumes it before publishing. The `go.work` file is a developer convenience and belongs in `.gitignore` unless the repository is a deliberate monorepo.

```sh
go work init ./api ./lib            # creates go.work listing both modules
go work use ./tools                 # add a module
go work sync                        # push the workspace's chosen versions back into each go.mod
GOWORK=off go build ./...           # build as CI would, ignoring the workspace
```

Build constraints select files per platform or feature. A `//go:build` line must sit before the package clause with a blank line after it; filename suffixes (`_linux.go`, `_amd64.go`, `_linux_amd64.go`, `_test.go`) apply the same constraints implicitly.

```go
//go:build linux && !integration

package store
```

```sh
go test -tags=integration ./...     # compile files constrained by //go:build integration
go vet -tags=integration ./...      # vet the same set; untagged runs never see those files
```

`embed` compiles files into the binary at build time so a service ships its migrations, templates and static assets in one artefact. Paths are relative to the source file's directory, cannot contain `..`, and by default skip files beginning with `.` or `_` (prefix the pattern with `all:` to include them).

```go
import "embed"

//go:embed migrations/*.sql
var migrations embed.FS               // read with fs.ReadFile(migrations, "migrations/001_init.sql")

//go:embed VERSION
var version string                    // a single file may embed straight into a string or []byte

//go:embed all:static
var static embed.FS                   // include dotfiles under static/
http.Handle("/static/", http.FileServerFS(static))   // Go 1.22+: serve an fs.FS directly
```

## golangci-lint

`golangci-lint` runs many linters in one pass with shared parsing and caching. Version 2 (2025) changed the config format: the file must declare `version: "2"`, `linters.default` picks the base set (`standard`, `all`, `none`, `fast`), and formatters (`gofmt`, `gofumpt`, `goimports`, `gci`) moved to their own `formatters` block and run with `golangci-lint fmt`.

```yaml
# .golangci.yml
version: "2"
run:
  timeout: 5m
  tests: true
linters:
  default: standard                # errcheck, govet, ineffassign, staticcheck, unused
  enable:
    - bodyclose                    # HTTP response bodies left open
    - contextcheck                 # functions that drop the incoming ctx
    - errorlint                    # == on errors, %v where %w is needed
    - gosec
    - nilerr                       # returns nil after checking err != nil
    - sqlclosecheck
  settings:
    errcheck:
      check-type-assertions: true
  exclusions:
    paths:
      - third_party$
formatters:
  enable:
    - gofumpt
    - goimports
```

```sh
golangci-lint run ./...                  # lint; exit 1 on findings
golangci-lint run --new-from-rev=origin/main   # only issues introduced on this branch
golangci-lint fmt                        # apply the configured formatters
golangci-lint migrate                    # convert a v1 config to v2
golangci-lint linters                    # which linters are enabled with the current config
```

Pin the linter version in CI (`golangci/golangci-lint-action` with `version: v2.x`) so a new release does not fail the build with new checks on an unrelated day. Silence a single false positive with `//nolint:gosec // reason` on the line; a bare `//nolint` without a linter name is itself flagged by `nolintlint`.

## Troubleshooting

| Symptom | Cause | Check |
| --- | --- | --- |
| `missing go.sum entry` | `go.mod` changed without a tidy | `go mod tidy`, then commit `go.sum` |
| `fatal error: concurrent map ...` | Concurrent read and write of a plain map | `go test -race`; guard with a mutex or use `sync.Map` |
| Test passes locally, fails in CI | Shared state or ordering between tests, or a real race | `go test -race -count=10`; remove global state |
| Goroutine count climbs without bound | A goroutine blocked on a channel or ignoring `ctx` | `curl localhost:6060/debug/pprof/goroutine?debug=2` |
| `context deadline exceeded` on every call | Timeout too short, or `cancel()` fires early | Check the `WithTimeout` value and the scope of `defer cancel()` |
| Binary far larger than expected | Debug symbols and DWARF included | Build with `-ldflags='-s -w'`; inspect with `go tool nm -size -sort size` |
| `go: updates to go.mod needed` in CI | The build wants to change dependencies | `go mod tidy` locally; run CI with `-mod=readonly` |
| High GC CPU, sawtooth heap | Excess short-lived allocations | `GODEBUG=gctrace=1`; find escapes with `-gcflags='-m'`, then pool or preallocate |
| `govulncheck` reports a hit | A reachable call into a vulnerable symbol | `go get pkg@fixed`, then re-run `govulncheck ./...` |
| `pattern ... : no matching files found` on `//go:embed` | Path is wrong, outside the package directory, or matched only dotfiles | Paths are relative to the source file; use `all:` for `.`/`_` names |
| Test file silently not compiled | `//go:build` tag not passed, or a missing blank line after the constraint | `go list -tags=integration -f '{{.TestGoFiles}}' ./pkg` |
| `go build` uses a local module version CI does not have | A `go.work` file in a parent directory | `go env GOWORK`; build with `GOWORK=off` |
| `unsupported version of the configuration` from golangci-lint | v1 config with a v2 binary | `golangci-lint migrate`, then commit the `version: "2"` file |
| `cannot infer T` | Type parameter appears only in the return type | Instantiate explicitly: `Zero[string]()` |
| `panic: test timed out after 10m0s` | A test deadlocked on a channel or lock | Read the goroutine dump printed with the panic; add `-timeout` per package |
| `-race` slows tests 5-10x and CI times out | Race detector overhead | Run `-race` on the concurrent packages only, or in a separate job with a longer timeout |
| Old test result reported after a fix | Cached test output | `go test -count=1`, or `go clean -testcache` |

## Oneliners

```sh
# Every exported symbol in a package
go doc -all ./pkg/store | grep -E '^func|^type'

# Which packages pull in a module and why
go mod why -m golang.org/x/net

# Build every main package in the repository
go build ./... && go list -f '{{if eq .Name "main"}}{{.ImportPath}}{{end}}' ./...

# Find heap escapes in hot code
go build -gcflags='-m -m' ./pkg/hot 2>&1 | grep 'escapes to heap'

# Test only packages that changed against main
go test $(git diff --name-only origin/main | grep '\.go$' | xargs -r -n1 dirname | sort -u | sed 's|^|./|')

# Fail the build on unformatted files
test -z "$(gofmt -l .)" || { gofmt -l .; exit 1; }

# Race-test a single package repeatedly to catch flakes
go test -race -count=50 -run TestConcurrent ./pkg/queue

# Binary size by symbol
go tool nm -size -sort size ./api | head -20

# What the compiler inlined
go build -gcflags='-m' ./... 2>&1 | grep 'can inline'

# Module versions embedded in a built binary
go version -m ./api | grep dep

# Regenerate code and fail if the result is not committed
go generate ./... && git diff --exit-code

# Every test in the module that is not run in parallel
grep -rL 't.Parallel()' --include='*_test.go' .

# Which of my dependencies have a newer minor or patch release
go list -m -u -f '{{if .Update}}{{.Path}} {{.Version}} -> {{.Update.Version}}{{end}}' all

# Build once, then run the same binary under several inputs
go build -o ./bin/api ./cmd/api && ./bin/api -config dev.yaml
```

## Snippets

Retry with exponential backoff and jitter, stopping on context cancellation or a permanent error.

```go
func retry(ctx context.Context, attempts int, fn func() error) error {
    var err error
    for i := 0; i < attempts; i++ {
        if err = fn(); err == nil || errors.Is(err, errPermanent) {
            return err
        }
        delay := time.Duration(1<<i)*200*time.Millisecond + time.Duration(rand.IntN(100))*time.Millisecond
        select {
        case <-time.After(delay):
        case <-ctx.Done():
            return fmt.Errorf("retry: %w (last: %v)", ctx.Err(), err)
        }
    }
    return fmt.Errorf("after %d attempts: %w", attempts, err)
}
```

Bounded parallelism with `errgroup.SetLimit`, so a slice of 10,000 jobs never has more than 16 in flight.

```go
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(16)
for _, u := range urls {
    g.Go(func() error { return fetch(ctx, u) })
}
err := g.Wait()
```

Per-call timeout around a dependency, with the deadline visible in the error.

```go
ctx, cancel := context.WithTimeoutCause(ctx, 2*time.Second, errors.New("db lookup budget"))   // Go 1.21+
defer cancel()
row, err := db.QueryRowContext(ctx, q, id)
if err != nil && context.Cause(ctx) != nil {
    return fmt.Errorf("lookup: %w", context.Cause(ctx))   // "db lookup budget" instead of a bare deadline error
}
```

Configuration from environment with defaults and validation, no third-party library.

```go
type Config struct {
    Addr    string
    DBURL   string
    Timeout time.Duration
}

func Load() (Config, error) {
    c := Config{Addr: envOr("ADDR", ":8080"), DBURL: os.Getenv("DATABASE_URL")}
    d, err := time.ParseDuration(envOr("TIMEOUT", "5s"))
    if err != nil { return c, fmt.Errorf("TIMEOUT: %w", err) }
    c.Timeout = d
    if c.DBURL == "" { return c, errors.New("DATABASE_URL is required") }
    return c, nil
}

func envOr(k, def string) string { if v := os.Getenv(k); v != "" { return v }; return def }
```

HTTP client with a transport-level timeout for each phase, not just the whole request.

```go
client := &http.Client{
    Timeout: 30 * time.Second,
    Transport: &http.Transport{
        DialContext:           (&net.Dialer{Timeout: 3 * time.Second}).DialContext,
        TLSHandshakeTimeout:   5 * time.Second,
        ResponseHeaderTimeout: 10 * time.Second,     // time to first byte of the response
        MaxIdleConnsPerHost:   50,
        IdleConnTimeout:       90 * time.Second,
    },
}
```

Decode a JSON response body with a size cap and unknown-field rejection.

```go
dec := json.NewDecoder(io.LimitReader(resp.Body, 1<<20))   // refuse bodies over 1 MiB
dec.DisallowUnknownFields()
var out Payload
if err := dec.Decode(&out); err != nil {
    return fmt.Errorf("decode %s: %w", resp.Request.URL, err)
}
```

Graceful shutdown on SIGINT or SIGTERM with a drain deadline.

```go
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

go func() { _ = srv.ListenAndServe() }()
<-ctx.Done()
stop()                                                     // restore default signal handling: a second Ctrl-C kills
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
    log.Error("shutdown", "err", err)                     // in-flight requests exceeded the deadline
}
```

Structured logging with `log/slog`: JSON to stderr, a request-scoped logger with fields attached once.

```go
logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo, AddSource: true}))
slog.SetDefault(logger)

reqLog := logger.With("request_id", id, "path", r.URL.Path)
reqLog.InfoContext(ctx, "handled", "status", status, "duration", time.Since(start))
reqLog.Error("upstream", "err", err)                      // err renders as its message; wrap in slog.Any for the type
```

HTTP middleware that recovers panics and logs each request.

```go
func logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        defer func() {
            if p := recover(); p != nil {
                slog.Error("panic", "err", p, "stack", string(debug.Stack()))
                http.Error(w, "internal error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
        slog.Info("request", "method", r.Method, "path", r.URL.Path, "ms", time.Since(start).Milliseconds())
    })
}
```

Method and path patterns in the standard mux (Go 1.22+), with path values.

```go
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    _ = id
})
mux.HandleFunc("POST /users", createUser)
mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServerFS(static)))
```

A ticker loop that stops cleanly and never fires two overlapping runs.

```go
t := time.NewTicker(time.Minute)
defer t.Stop()
for {
    select {
    case <-ctx.Done():
        return
    case <-t.C:
        if err := sweep(ctx); err != nil { slog.Warn("sweep", "err", err) }   // runs serially; a slow sweep skips ticks
    }
}
```

Table-driven test with a fake dependency and `t.Context()` (Go 1.24+).

```go
type fakeStore struct{ users map[int64]*User }

func (f fakeStore) Get(_ context.Context, id int64) (*User, error) {
    u, ok := f.users[id]
    if !ok { return nil, ErrNotFound }
    return u, nil
}

func TestHandler(t *testing.T) {
    h := NewHandler(fakeStore{users: map[int64]*User{1: {Name: "ann"}}})
    for _, tt := range []struct{ path string; want int }{{"/users/1", 200}, {"/users/9", 404}} {
        t.Run(tt.path, func(t *testing.T) {
            req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, tt.path, nil)
            rec := httptest.NewRecorder()
            h.ServeHTTP(rec, req)
            if rec.Code != tt.want { t.Errorf("status = %d, want %d", rec.Code, tt.want) }
        })
    }
}
```

Golden-file comparison with an `-update` flag, so expected output lives beside the test.

```go
var update = flag.Bool("update", false, "rewrite golden files")

func TestRender(t *testing.T) {
    got := Render(input)
    golden := filepath.Join("testdata", t.Name()+".golden")
    if *update { os.WriteFile(golden, got, 0o644) }
    want, err := os.ReadFile(golden)
    if err != nil { t.Fatal(err) }
    if !bytes.Equal(got, want) { t.Errorf("output differs from %s; run with -update to accept", golden) }
}
```

Read a large file line by line without loading it, with a raised buffer for long lines.

```go
sc := bufio.NewScanner(f)
sc.Buffer(make([]byte, 0, 1024*1024), 1024*1024)   // default token limit is 64 KiB
for sc.Scan() {
    line := sc.Text()
    _ = line
}
if err := sc.Err(); err != nil { return err }       // bufio.ErrTooLong when a line exceeds the buffer
```

Atomic file write: temp file in the same directory, fsync, rename.

```go
func writeAtomic(path string, data []byte) error {
    tmp, err := os.CreateTemp(filepath.Dir(path), ".tmp-*")
    if err != nil { return err }
    defer os.Remove(tmp.Name())                        // no-op after a successful rename
    if _, err := tmp.Write(data); err != nil { tmp.Close(); return err }
    if err := tmp.Sync(); err != nil { tmp.Close(); return err }
    if err := tmp.Close(); err != nil { return err }
    return os.Rename(tmp.Name(), path)
}
```

Run a subprocess with a timeout and capture both streams separately.

```go
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "pg_dump", "--format=custom", dbURL)
cmd.WaitDelay = 5 * time.Second                    // Go 1.20+: grace period before SIGKILL after ctx ends
var stdout, stderr bytes.Buffer
cmd.Stdout, cmd.Stderr = &stdout, &stderr
if err := cmd.Run(); err != nil {
    return fmt.Errorf("pg_dump: %w: %s", err, strings.TrimSpace(stderr.String()))
}
```

Custom error type that carries a status code and still supports `errors.Is`.

```go
type HTTPError struct {
    Status int
    Err    error
}

func (e *HTTPError) Error() string { return fmt.Sprintf("http %d: %v", e.Status, e.Err) }
func (e *HTTPError) Unwrap() error { return e.Err }

// caller
var he *HTTPError
if errors.As(err, &he) && he.Status == http.StatusTooManyRequests { backoff() }
```


