|
| 1 | +# Modern Go (1.18–1.26) |
| 2 | + |
| 3 | +Reference for writing idiomatic Go. Covers what changed, what it |
| 4 | +replaced, and what to reach for. Respect the project's `go.mod` `go` |
| 5 | +line: don't emit features from a version newer than what the module |
| 6 | +declares. Check `go.mod` before writing code. |
| 7 | + |
| 8 | +## How modern Go thinks differently |
| 9 | + |
| 10 | +**Generics** (1.18): Design reusable code with type parameters instead |
| 11 | +of `interface{}` casts, code generation, or the `sort.Interface` |
| 12 | +pattern. Use `any` for unconstrained types, `comparable` for map keys |
| 13 | +and equality, `cmp.Ordered` for sortable types. Type inference usually |
| 14 | +makes explicit type arguments unnecessary (improved in 1.21). |
| 15 | + |
| 16 | +**Per-iteration loop variables** (1.22): Each loop iteration gets its |
| 17 | +own variable copy. Closures inside loops capture the correct value. The |
| 18 | +`v := v` shadow trick is dead. Remove it when you see it. |
| 19 | + |
| 20 | +**Iterators** (1.23): `iter.Seq[V]` and `iter.Seq2[K,V]` are the |
| 21 | +standard iterator types. Containers expose `.All()` methods returning |
| 22 | +these. Combined with `slices.Collect`, `slices.Sorted`, `maps.Keys`, |
| 23 | +etc., they replace ad-hoc "loop and append" code with composable, |
| 24 | +lazy pipelines. When a sequence is consumed only once, prefer an |
| 25 | +iterator over materializing a slice. |
| 26 | + |
| 27 | +**Error trees** (1.20–1.26): Errors compose as trees, not chains. |
| 28 | +`errors.Join` aggregates multiple errors. `fmt.Errorf` accepts multiple |
| 29 | +`%w` verbs. `errors.Is`/`As` traverse the full tree. Custom error |
| 30 | +types that wrap multiple causes must implement `Unwrap() []error` (the |
| 31 | +slice form), not `Unwrap() error`, or tree traversal won't find the |
| 32 | +children. `errors.AsType[T]` (1.26) is the type-safe way to match |
| 33 | +error types. Propagate cancellation reasons with |
| 34 | +`context.WithCancelCause`. |
| 35 | + |
| 36 | +**Structured logging** (1.21): `log/slog` is the standard structured |
| 37 | +logger. This project uses `cdr.dev/slog/v3` instead, which has a |
| 38 | +different API. Do not use `log/slog` directly. |
| 39 | + |
| 40 | +## Replace these patterns |
| 41 | + |
| 42 | +The left column reflects common patterns from pre-1.22 Go. Write the |
| 43 | +right column instead. The "Since" column tells you the minimum `go` |
| 44 | +directive version required in `go.mod`. |
| 45 | + |
| 46 | +| Old pattern | Modern replacement | Since | |
| 47 | +|---|---|---| |
| 48 | +| `interface{}` | `any` | 1.18 | |
| 49 | +| `v := v` inside loops | remove it | 1.22 | |
| 50 | +| `for i := 0; i < n; i++` | `for i := range n` | 1.22 | |
| 51 | +| `for i := 0; i < b.N; i++` (benchmarks) | `for b.Loop()` (correct timing, future-proof) | 1.24 | |
| 52 | +| `sort.Slice(s, func(i,j int) bool{…})` | `slices.SortFunc(s, cmpFn)` | 1.21 | |
| 53 | +| `wg.Add(1); go func(){ defer wg.Done(); … }()` | `wg.Go(func(){…})` | 1.25 | |
| 54 | +| `func ptr[T any](v T) *T { return &v }` | `new(expr)` e.g. `new(time.Now())` | 1.26 | |
| 55 | +| `var target *E; errors.As(err, &target)` | `t, ok := errors.AsType[*E](err)` | 1.26 | |
| 56 | +| Custom multi-error type | `errors.Join(err1, err2, …)` | 1.20 | |
| 57 | +| Single `%w` for multiple causes | `fmt.Errorf("…: %w, %w", e1, e2)` | 1.20 | |
| 58 | +| `rand.Seed(time.Now().UnixNano())` | delete it (auto-seeded); prefer `math/rand/v2` | 1.20/1.22 | |
| 59 | +| `sync.Once` + captured variable | `sync.OnceValue(func() T {…})` / `OnceValues` | 1.21 | |
| 60 | +| Custom `min`/`max` helpers | `min(a, b)` / `max(a, b)` builtins (any ordered type) | 1.21 | |
| 61 | +| `for k := range m { delete(m, k) }` | `clear(m)` (also zeroes slices) | 1.21 | |
| 62 | +| Index+slice or `SplitN(s, sep, 2)` | `strings.Cut(s, sep)` / `bytes.Cut` | 1.18 | |
| 63 | +| `TrimPrefix` + check if anything was trimmed | `strings.CutPrefix` / `CutSuffix` (returns ok bool) | 1.20 | |
| 64 | +| `strings.Split` + loop when no slice is needed | `strings.SplitSeq` / `Lines` / `FieldsSeq` (iterator, no alloc) | 1.24 | |
| 65 | +| `"2006-01-02"` / `"2006-01-02 15:04:05"` / `"15:04:05"` | `time.DateOnly` / `time.DateTime` / `time.TimeOnly` | 1.20 | |
| 66 | +| Manual `Before`/`After`/`Equal` chains for comparison | `time.Time.Compare` (returns -1/0/+1; works with `slices.SortFunc`) | 1.20 | |
| 67 | +| Loop collecting map keys into slice | `slices.Sorted(maps.Keys(m))` | 1.23 | |
| 68 | +| `fmt.Sprintf` + append to `[]byte` | `fmt.Appendf(buf, …)` (also `Append`, `Appendln`) | 1.18 | |
| 69 | +| `reflect.TypeOf((*T)(nil)).Elem()` | `reflect.TypeFor[T]()` | 1.22 | |
| 70 | +| `*(*[4]byte)(slice)` unsafe cast | `[4]byte(slice)` direct conversion | 1.20 | |
| 71 | +| `atomic.LoadInt64` / `StoreInt64` | `atomic.Int64` (also `Bool`, `Uint64`, `Pointer[T]`) | 1.19 | |
| 72 | +| `crypto/rand.Read(buf)` + hex/base64 encode | `crypto/rand.Text()` (one call) | 1.24 | |
| 73 | +| Checking `crypto/rand.Read` error | don't: return is always nil | 1.24 | |
| 74 | +| `time.Sleep` in tests | `testing/synctest` (deterministic fake clock) | 1.24/1.25 | |
| 75 | +| `json:",omitempty"` on zero-value structs like `time.Time{}` | `json:",omitzero"` (uses `IsZero()` method) | 1.24 | |
| 76 | +| `strings.Title` | `golang.org/x/text/cases` | 1.18 | |
| 77 | +| `net.IP` in new code | `net/netip.Addr` (immutable, comparable, lighter) | 1.18 | |
| 78 | +| `tools.go` with blank imports | `tool` directive in `go.mod` | 1.24 | |
| 79 | +| `runtime.SetFinalizer` | `runtime.AddCleanup` (multiple per object, no pointer cycles) | 1.24 | |
| 80 | +| `httputil.ReverseProxy.Director` | `.Rewrite` hook + `ProxyRequest` (Director deprecated in 1.26) | 1.20 | |
| 81 | +| `sql.NullString`, `sql.NullInt64`, etc. | `sql.Null[T]` | 1.22 | |
| 82 | +| Manual `ctx, cancel := context.WithCancel(…)` + `t.Cleanup(cancel)` | `t.Context()` (auto-canceled when test ends) | 1.24 | |
| 83 | +| `if d < 0 { d = -d }` on durations | `d.Abs()` (handles `math.MinInt64`) | 1.19 | |
| 84 | +| Implement only `TextMarshaler` | also implement `TextAppender` for alloc-free marshaling | 1.24 | |
| 85 | +| Custom `Unwrap() error` on multi-cause errors | `Unwrap() []error` (slice form; required for tree traversal) | 1.20 | |
| 86 | + |
| 87 | +## New capabilities |
| 88 | + |
| 89 | +These enable things that weren't practical before. Reach for them in the |
| 90 | +described situations. |
| 91 | + |
| 92 | +| What | Since | When to use it | |
| 93 | +|---|---|---| |
| 94 | +| `cmp.Or(a, b, c)` | 1.22 | Defaults/fallback chains: returns first non-zero value. Replaces verbose `if a != "" { return a }` cascades. | |
| 95 | +| `context.WithoutCancel(ctx)` | 1.21 | Background work that must outlive the request (e.g. async cleanup after HTTP response). Derived context keeps parent's values but ignores cancellation. | |
| 96 | +| `context.AfterFunc(ctx, fn)` | 1.21 | Register cleanup that fires on context cancellation without spawning a goroutine that blocks on `<-ctx.Done()`. | |
| 97 | +| `context.WithCancelCause` / `Cause` | 1.20 | When callers need to know WHY a context was canceled, not just that it was. Retrieve cause with `context.Cause(ctx)`. | |
| 98 | +| `context.WithDeadlineCause` / `WithTimeoutCause` | 1.21 | Attach a domain-specific error to deadline/timeout expiry (e.g. distinguish "DB query timed out" from "HTTP request timed out"). | |
| 99 | +| `errors.ErrUnsupported` | 1.21 | Standard sentinel for "not supported." Use instead of per-package custom sentinels. Check with `errors.Is`. | |
| 100 | +| `http.ResponseController` | 1.20 | Per-request flush, hijack, and deadline control without type-asserting `ResponseWriter` to `http.Flusher` or `http.Hijacker`. | |
| 101 | +| Enhanced `ServeMux` routing | 1.22 | `"GET /items/{id}"` patterns in `http.ServeMux`. Access with `r.PathValue("id")`. Wildcards: `{name}`, catch-all: `{path...}`, exact: `{$}`. Eliminates many third-party router dependencies. | |
| 102 | +| `os.Root` / `OpenRoot` | 1.24 | Confined directory access that prevents symlink escape. 1.25 adds `MkdirAll`, `ReadFile`, `WriteFile` for real use. | |
| 103 | +| `os.CopyFS` | 1.23 | Copy an entire `fs.FS` to local filesystem in one call. | |
| 104 | +| `os/signal.NotifyContext` with cause | 1.26 | Cancellation cause identifies which signal (SIGTERM vs SIGINT) triggered shutdown. | |
| 105 | +| `io/fs.SkipAll` / `filepath.SkipAll` | 1.20 | Return from `WalkDir` callback to stop walking entirely. Cleaner than a sentinel error. | |
| 106 | +| `GOMEMLIMIT` env / `debug.SetMemoryLimit` | 1.19 | Soft memory limit for GC. Use alongside or instead of `GOGC` in memory-constrained containers. | |
| 107 | +| `net/url.JoinPath` | 1.19 | Join URL path segments correctly. Replaces error-prone string concatenation. | |
| 108 | +| `go test -skip` | 1.20 | Skip tests matching a pattern. Useful when running a subset of a large test suite. | |
| 109 | + |
| 110 | +## Key packages |
| 111 | + |
| 112 | +### `slices` (1.21, iterators added 1.23) |
| 113 | + |
| 114 | +Replaces `sort.Slice`, manual search loops, and manual contains checks. |
| 115 | + |
| 116 | +Search: `Contains`, `ContainsFunc`, `Index`, `IndexFunc`, |
| 117 | +`BinarySearch`, `BinarySearchFunc`. |
| 118 | + |
| 119 | +Sort: `Sort`, `SortFunc`, `SortStableFunc`, `IsSorted`, `IsSortedFunc`, |
| 120 | +`Min`, `MinFunc`, `Max`, `MaxFunc`. |
| 121 | + |
| 122 | +Transform: `Clone`, `Compact`, `CompactFunc`, `Grow`, `Clip`, |
| 123 | +`Concat` (1.22), `Repeat` (1.23), `Reverse`, `Insert`, `Delete`, |
| 124 | +`Replace`. |
| 125 | + |
| 126 | +Compare: `Equal`, `EqualFunc`, `Compare`. |
| 127 | + |
| 128 | +Iterators (1.23): `All`, `Values`, `Backward`, `Collect`, `AppendSeq`, |
| 129 | +`Sorted`, `SortedFunc`, `SortedStableFunc`, `Chunk`. |
| 130 | + |
| 131 | +### `maps` (1.21, iterators added 1.23) |
| 132 | + |
| 133 | +Core: `Clone`, `Copy`, `Equal`, `EqualFunc`, `DeleteFunc`. |
| 134 | + |
| 135 | +Iterators (1.23): `All`, `Keys`, `Values`, `Insert`, `Collect`. |
| 136 | + |
| 137 | +### `cmp` (1.21, `Or` added 1.22) |
| 138 | + |
| 139 | +`Ordered` constraint for any ordered type. `Compare(a, b)` returns |
| 140 | +-1/0/+1. `Less(a, b)` returns bool. `Or(vals...)` returns first |
| 141 | +non-zero value. |
| 142 | + |
| 143 | +### `iter` (1.23) |
| 144 | + |
| 145 | +`Seq[V]` is `func(yield func(V) bool)`. `Seq2[K,V]` is |
| 146 | +`func(yield func(K, V) bool)`. Return these from your container's |
| 147 | +`.All()` methods. Consume with `for v := range seq` or pass to |
| 148 | +`slices.Collect`, `slices.Sorted`, `maps.Collect`, etc. |
| 149 | + |
| 150 | +### `math/rand/v2` (1.22) |
| 151 | + |
| 152 | +Replaces `math/rand`. `IntN` not `Intn`. Generic `N[T]()` for any |
| 153 | +integer type. Default source is `ChaCha8` (crypto-quality). No global |
| 154 | +`Seed`. Use `rand.New(source)` for reproducible sequences. |
| 155 | + |
| 156 | +### `log/slog` (1.21) |
| 157 | + |
| 158 | +`slog.Info`, `slog.Warn`, `slog.Error`, `slog.Debug` with key-value |
| 159 | +pairs. `slog.With(attrs...)` for logger with preset fields. |
| 160 | +`slog.GroupAttrs` (1.25) for clean group creation. Implement |
| 161 | +`slog.Handler` for custom backends. |
| 162 | + |
| 163 | +**Note:** This project uses `cdr.dev/slog/v3`, not `log/slog`. The |
| 164 | +API is different. Read existing code for usage patterns. |
| 165 | + |
| 166 | +## Pitfalls |
| 167 | + |
| 168 | +Things that are easy to get wrong, even when you know the modern API |
| 169 | +exists. Check your output against these. |
| 170 | + |
| 171 | +**Version misuse.** The replacement table has a "Since" column. If the |
| 172 | +project's `go.mod` says `go 1.22`, you cannot use `wg.Go` (1.25), |
| 173 | +`errors.AsType` (1.26), `new(expr)` (1.26), `b.Loop()` (1.24), or |
| 174 | +`testing/synctest` (1.24). Fall back to the older pattern. Always |
| 175 | +check before reaching for a replacement. |
| 176 | + |
| 177 | +**`slices.Sort` vs `slices.SortFunc`.** `slices.Sort` requires |
| 178 | +`cmp.Ordered` types (int, string, float64, etc.). For structs, custom |
| 179 | +types, or multi-field sorting, use `slices.SortFunc` with a comparator |
| 180 | +function. Using `slices.Sort` on a non-ordered type is a compile error. |
| 181 | + |
| 182 | +**`for range n` still binds the index.** `for range n` discards the |
| 183 | +index. If you need it, write `for i := range n`. Writing |
| 184 | +`for range n` and then trying to use `i` inside the loop is a compile |
| 185 | +error. |
| 186 | + |
| 187 | +**Don't hand-roll iterators when the stdlib returns one.** Functions |
| 188 | +like `maps.Keys`, `slices.Values`, `strings.SplitSeq`, and |
| 189 | +`strings.Lines` already return `iter.Seq` or `iter.Seq2`. Don't |
| 190 | +reimplement them. Compose with `slices.Collect`, `slices.Sorted`, etc. |
| 191 | + |
| 192 | +**Don't mix `math/rand` and `math/rand/v2`.** They have different |
| 193 | +function names (`Intn` vs `IntN`) and different default sources. Pick |
| 194 | +one per package. Prefer v2 for new code. The v1 global source is |
| 195 | +auto-seeded since 1.20, so delete `rand.Seed` calls either way. |
| 196 | + |
| 197 | +**Iterator protocol.** When implementing `iter.Seq`, you must respect |
| 198 | +the `yield` return value. If `yield` returns `false`, stop iteration |
| 199 | +immediately and return. Ignoring it violates the contract and causes |
| 200 | +panics when consumers break out of `for range` loops early. |
| 201 | + |
| 202 | +**`errors.Join` with nil.** `errors.Join` skips nil arguments. This is |
| 203 | +intentional and useful for aggregating optional errors, but don't |
| 204 | +assume the result is always non-nil. `errors.Join(nil, nil)` returns |
| 205 | +nil. |
| 206 | + |
| 207 | +**`cmp.Or` evaluates all arguments.** Unlike a chain of `if` |
| 208 | +statements, `cmp.Or(a(), b(), c())` calls all three functions. If any |
| 209 | +have side effects or are expensive, use `if`/`else` instead. |
| 210 | + |
| 211 | +**Timer channel semantics changed in 1.23.** Code that checks |
| 212 | +`len(timer.C)` to see if a value is pending no longer works (channel |
| 213 | +capacity is 0). Use a non-blocking `select` receive instead: |
| 214 | +`select { case <-timer.C: default: }`. |
| 215 | + |
| 216 | +**`context.WithoutCancel` still propagates values.** The derived |
| 217 | +context inherits all values from the parent. If any middleware stores |
| 218 | +request-scoped state (deadlines, trace IDs) via `context.WithValue`, |
| 219 | +the background work sees it. This is usually desired but can be |
| 220 | +surprising if the values hold references that should not outlive the |
| 221 | +request. |
| 222 | + |
| 223 | +## Behavioral changes that affect code |
| 224 | + |
| 225 | +- **Timers** (1.23): unstopped `Timer`/`Ticker` are GC'd immediately. |
| 226 | + Channels are unbuffered: no stale values after `Reset`/`Stop`. You no |
| 227 | + longer need `defer t.Stop()` to prevent leaks. |
| 228 | +- **Error tree traversal** (1.20): `errors.Is`/`As` follow |
| 229 | + `Unwrap() []error`, not just `Unwrap() error`. Multi-error types must |
| 230 | + expose the slice form for child errors to be found. |
| 231 | +- **`math/rand` auto-seeded** (1.20): the global RNG is auto-seeded. |
| 232 | + `rand.Seed` is a no-op in 1.24+. Don't call it. |
| 233 | +- **GODEBUG compat** (1.21): behavioral changes are gated by `go.mod`'s |
| 234 | + `go` line. Upgrading the version opts into new defaults. |
| 235 | +- **Build tags** (1.18): `//go:build` is the only syntax. `// +build` |
| 236 | + is gone. |
| 237 | +- **Tool install** (1.18): `go get` no longer builds. Use |
| 238 | + `go install pkg@version`. |
| 239 | +- **Doc comments** (1.19): support `[links]`, lists, and headings. |
| 240 | +- **`go test -skip`** (1.20): skip tests by name pattern from the |
| 241 | + command line. |
| 242 | +- **`go fix ./...` modernizers** (1.26): auto-rewrites code to use |
| 243 | + newer idioms. Run after Go version upgrades. |
| 244 | + |
| 245 | +## Transparent improvements (no code changes) |
| 246 | + |
| 247 | +Swiss Tables maps, Green Tea GC, PGO, faster `io.ReadAll`, |
| 248 | +stack-allocated slices, reduced cgo overhead, container-aware |
| 249 | +GOMAXPROCS. Free on upgrade. |
0 commit comments