# Go Version Modernizations ## Go 1.21 Modernizations (August 2023) Changelog: ### Use built-in `min`, `max`, `clear` _(Go 1.21+)_ Remove custom implementations. `min`/`max` work with any ordered type and accept variadic arguments: ```go // Before func minInt(a, b int) int { if a < b { return a } return b } x := minInt(a, b) // After (Go 1.21+) x := min(a, b) smallest := min(a, b, c, d) ``` `clear` zeroes maps and slices: ```go // Before for k := range m { delete(m, k) } // After (Go 1.21+) clear(m) ``` ### Use `log/slog` instead of third-party loggers _(Go 1.21+)_ `log/slog` is the standard structured logging package. New code SHOULD migrate to `slog` over `zap`, `logrus`, or `zerolog`. ```go // Before: zap logger, _ := zap.NewProduction() logger.Info("request handled", zap.String("method", r.Method), zap.Int("status", status)) // Before: logrus logrus.WithFields(logrus.Fields{"method": r.Method, "status": status}).Info("request handled") // After (Go 1.21+): slog slog.Info("request handled", "method", r.Method, "status", status) // Or with type-safe attributes: slog.Info("request handled", slog.String("method", r.Method), slog.Int("status", status)) ``` **Migration guidance**: For existing projects heavily invested in third-party loggers, migration is optional. For new projects, prefer `slog`. The `samber/slog-*` ecosystem provides handlers for routing slog output to various backends. Go 1.24 added `slog.DiscardHandler` for silent loggers. ### Use `slices` package instead of `sort` and manual loops _(Go 1.21+)_ ```go // Before sort.Strings(names) sort.Slice(users, func(i, j int) bool { return users[i].Name < users[j].Name }) // After (Go 1.21+) slices.Sort(names) slices.SortFunc(users, func(a, b User) int { return cmp.Compare(a.Name, b.Name) }) ``` ```go // Before: manual search found := false for _, v := range items { if v == target { found = true; break } } // After (Go 1.21+) found := slices.Contains(items, target) ``` ```go // Before: manual clone clone := append([]string(nil), original...) // After (Go 1.21+) clone := slices.Clone(original) ``` ### Use `maps` package _(Go 1.21+)_ ```go // Before clone := make(map[string]int, len(original)) for k, v := range original { clone[k] = v } // After (Go 1.21+) clone := maps.Clone(original) ``` ### Use `cmp.Or` for default values _(Go 1.22+)_ ```go // Before addr := os.Getenv("ADDR") if addr == "" { addr = ":8080" } // After (Go 1.22+) addr := cmp.Or(os.Getenv("ADDR"), ":8080") ``` ### Use `sync.OnceFunc`, `sync.OnceValue`, `sync.OnceValues` _(Go 1.21+)_ ```go // Before var ( once sync.Once client *http.Client ) func getClient() *http.Client { once.Do(func() { client = &http.Client{Timeout: 10 * time.Second} }) return client } // After (Go 1.21+) var getClient = sync.OnceValue(func() *http.Client { return &http.Client{Timeout: 10 * time.Second} }) ``` ### Use enhanced `context` functions _(Go 1.21+)_ ```go ctx := context.WithoutCancel(parent) // detach from parent cancellation ctx, cancel := context.WithTimeoutCause(parent, 5*time.Second, errTimeout) ctx, cancel := context.WithDeadlineCause(parent, deadline, errDeadline) stop := context.AfterFunc(ctx, func() { cleanup() }) ``` --- ## Go 1.22 Modernizations (February 2024) Changelog: ### SHOULD use `range` over integers _(Go 1.22+)_ ```go // Before for i := 0; i < n; i++ { process(i) } // After (Go 1.22+) for i := range n { process(i) } // When index isn't needed for range 10 { fmt.Println("hello") } ``` ### Remove loop variable shadow copies _(Go 1.22+)_ Go 1.22 changed loop variable semantics: each iteration creates a new variable. Loop variable captures (`v := v`) SHOULD be removed in Go 1.22+ codebases. **Requirement**: The `go` directive in `go.mod` must be `go 1.22` or later for this behavior. ```go // Before (Go < 1.22) for _, v := range items { v := v // shadow copy to avoid closure bug go func() { process(v) }() } // After (Go 1.22+): safe by default for _, v := range items { go func() { process(v) }() } ``` ### `math/rand` MUST be replaced with `math/rand/v2` _(Go 1.22+)_ ```go // Before import "math/rand" rand.Seed(time.Now().UnixNano()) // no longer needed n := rand.Intn(100) // After (Go 1.22+) import "math/rand/v2" n := rand.IntN(100) // IntN, not Intn ``` Key `math/rand/v2` changes: - No global seed needed — automatically seeded - `Intn` -> `IntN`, `Int63n` -> `Int64N` (renamed) - `rand.N[T]()` generic function for any integer type - Better algorithms (ChaCha8, PCG) - `Read` removed — use `crypto/rand` for random bytes ### Use enhanced `net/http` routing _(Go 1.22+)_ ```go // Before: gorilla/mux or chi r := mux.NewRouter() r.HandleFunc("/users/{id}", getUser).Methods("GET") // After (Go 1.22+): stdlib mux := http.NewServeMux() mux.HandleFunc("GET /users/{id}", getUser) func getUser(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") } ``` ### Use `strings.CutPrefix` and `strings.CutSuffix` _(Go 1.20+)_ ```go // Before if strings.HasPrefix(s, "Bearer ") { token := strings.TrimPrefix(s, "Bearer ") } // After (Go 1.20+) if token, ok := strings.CutPrefix(s, "Bearer "); ok { // use token } ``` ### Use `reflect.TypeFor[T]()` _(Go 1.22+)_ ```go // Before t := reflect.TypeOf((*MyInterface)(nil)).Elem() // After (Go 1.22+) t := reflect.TypeFor[MyInterface]() ``` ### Use `database/sql.Null[T]` _(Go 1.22+)_ ```go // Before var name sql.NullString var age sql.NullInt64 // After (Go 1.22+) var name sql.Null[string] var age sql.Null[int64] ``` --- ## Go 1.23 Modernizations (August 2024) Changelog: ### Use iterators (`range` over functions) _(Go 1.23+)_ Go 1.23 introduced range-over-func with the `iter` package: ```go // Before: collect all results into a slice func AllUsers(db *sql.DB) ([]User, error) { rows, err := db.Query("SELECT ...") if err != nil { return nil, err } defer rows.Close() var users []User for rows.Next() { var u User rows.Scan(&u.ID, &u.Name) users = append(users, u) } return users, rows.Err() } // After (Go 1.23+): lazy iteration func AllUsers(db *sql.DB) iter.Seq2[User, error] { return func(yield func(User, error) bool) { rows, err := db.Query("SELECT ...") if err != nil { yield(User{}, err); return } defer rows.Close() for rows.Next() { var u User if err := rows.Scan(&u.ID, &u.Name); err != nil { yield(User{}, err); return } if !yield(u, nil) { return } } if err := rows.Err(); err != nil { yield(User{}, err) } } } ``` ### Use iterator-based `slices` and `maps` functions _(Go 1.23+)_ ```go // Sorted keys via iterator for k := range slices.Sorted(maps.Keys(m)) { fmt.Println(k, m[k]) } // Collect iterator into slice users := slices.Collect(maps.Values(userMap)) // Chunk a slice into batches for chunk := range slices.Chunk(items, 100) { processBatch(chunk) } ``` ### Use `unique` package for value interning _(Go 1.23+)_ ```go // Before: manual string interning var mu sync.Mutex var interned = make(map[string]string) // After (Go 1.23+) handle := unique.Make(s) // Handle[string], comparable, memory-efficient s = handle.Value() ``` ### Timer/Ticker behavior change _(Go 1.23+)_ With `go 1.23` or later in `go.mod`: - `time.Timer` and `time.Ticker` are garbage collected without calling `Stop()` - Timer channels are now unbuffered (capacity 0, was 1) Remove unnecessary `Stop()` calls in defer patterns where the timer goes out of scope. --- ## Go 1.24 Modernizations (February 2025) Changelog: ### Use generic type aliases _(Go 1.24+)_ ```go // Now valid (Go 1.24+) type Set[T comparable] = map[T]struct{} type Result[T any] = struct { Value T; Err error } ``` ### Use `os.Root` for directory-scoped file access _(Go 1.24+)_ **Security-critical**: `os.Root` prevents path traversal attacks (CWE-22) at the OS level. Replace all manual `filepath.Clean` + `strings.HasPrefix` validation with `os.Root` when handling user-supplied paths. Symlinks resolving outside the root are rejected. Supports `Open`, `Create`, `Stat`, `OpenFile`, `Mkdir`, `Remove`, and more. ```go // Before: manual path validation (risk of path traversal) path := filepath.Join(baseDir, userInput) data, err := os.ReadFile(path) // After (Go 1.24+): safe directory-scoped access root, err := os.OpenRoot("/opt/data") if err != nil { return err } defer root.Close() f, err := root.Open(userInput) // cannot escape root directory ``` ### Use `omitzero` JSON tag _(Go 1.24+)_ `omitzero` is more correct than `omitempty` for `time.Time`, `bool`, and custom types: ```go // Before: omitempty doesn't work well for time.Time type Event struct { At time.Time `json:"at,omitempty"` // zero time.Time is NOT omitted } // After (Go 1.24+) type Event struct { At time.Time `json:"at,omitzero"` // zero time.Time IS omitted } ``` ### Use `strings.SplitSeq`, `strings.FieldsSeq`, `strings.Lines` _(Go 1.24+)_ Iterator-returning variants avoid allocating `[]string`: ```go // Before: allocates a []string parts := strings.Split(csv, ",") for _, part := range parts { process(part) } // After (Go 1.24+): lazy, zero-allocation iteration for part := range strings.SplitSeq(csv, ",") { process(part) } ``` ### `t.Context()` SHOULD replace manual `context.Background()` in tests _(Go 1.24+)_ ```go // Before func TestFoo(t *testing.T) { ctx := context.Background() } // After (Go 1.24+): auto-cancelled when test ends func TestFoo(t *testing.T) { ctx := t.Context() } ``` ### `b.Loop()` MUST be used in benchmarks _(Go 1.24+)_ ```go // Before func BenchmarkFoo(b *testing.B) { for i := 0; i < b.N; i++ { foo() } } // After (Go 1.24+) func BenchmarkFoo(b *testing.B) { for b.Loop() { foo() } } ``` ### Use `runtime.AddCleanup` instead of `runtime.SetFinalizer` _(Go 1.24+)_ ```go // Before runtime.SetFinalizer(obj, func(o *Object) { o.Close() }) // After (Go 1.24+): more flexible, no cycle issues runtime.AddCleanup(obj, func(resource Resource) { resource.Close() }, obj.resource) ``` ### Use `weak` package for weak references _(Go 1.24+)_ ```go import "weak" ptr := weak.Make(obj) if v := ptr.Value(); v != nil { // object still alive } ``` ### Use `crypto/sha3`, `crypto/hkdf`, `crypto/pbkdf2` _(Go 1.24+)_ Replace `golang.org/x/crypto` sub-packages with standard library equivalents: ```go // Before import "golang.org/x/crypto/sha3" import "golang.org/x/crypto/hkdf" import "golang.org/x/crypto/pbkdf2" // After (Go 1.24+) import "crypto/sha3" import "crypto/hkdf" import "crypto/pbkdf2" ``` ### Use tool directives in `go.mod` _(Go 1.24+)_ Use `tool` directives instead of `tools.go` blank imports. ```bash go get -tool golang.org/x/tools/cmd/stringer@latest go get -tool github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest go tool stringer -type=Kind go tool golangci-lint run ./... ``` `go.mod` shape for a module targeting Go 1.26 or newer. This is an example target, not a cap; keep the project's actual `go` directive and do not change it just to add tools. ```go.mod module example.com/project go 1.26 tool ( golang.org/x/tools/cmd/stringer github.com/golangci/golangci-lint/v2/cmd/golangci-lint ) ``` Use `go install tool` to install all module-pinned tools when needed and `go get -u tool` to update them deliberately. ### Use `fmt.Appendf`, `fmt.Appendln` _(Go 1.19+, often overlooked)_ ```go // Before buf = append(buf, fmt.Sprintf("count: %d", n)...) // After (Go 1.19+) buf = fmt.Appendf(buf, "count: %d", n) ``` --- ## Go 1.25 Modernizations (August 2025) Changelog: ### Use `sync.WaitGroup.Go` _(Go 1.25+)_ ```go // Before var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() process() }() wg.Wait() // After (Go 1.25+) var wg sync.WaitGroup wg.Go(func() { process() }) wg.Wait() ``` ### Use `testing/synctest` for concurrent code testing _(Go 1.25+, experimental in 1.24)_ ```go // Before func TestConcurrent(t *testing.T) { var count atomic.Int32 var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() count.Add(1) }() wg.Wait() // Problem: Race conditions are hard to detect, timing-dependent, // and flaky tests are common if count.Load() != 1 { t.Fatal("expected 1") } } // After (Go 1.25+) func TestConcurrent(t *testing.T) { synctest.Test(t, func(t *testing.T) { var count atomic.Int32 go func() { count.Add(1) }() synctest.Wait() // wait for all goroutines to park if count.Load() != 1 { t.Fatal("expected 1") } }) } ``` **Note**: Use `synctest.Test` in Go 1.25+ and Go 1.26+. Do not use the old Go 1.24 experimental `synctest.Run` API in Go 1.25+ code. ### Use `runtime/trace.FlightRecorder` _(Go 1.25+)_ Lightweight always-on ring-buffer tracing for production: ```go fr := trace.NewFlightRecorder(trace.FlightRecorderConfig{}) if err := fr.Start(); err != nil { return err } // ... later, on error: fr.WriteTo(file) // captures recent trace data ``` ### Container-aware `GOMAXPROCS` _(Go 1.25+)_ Go 1.25 automatically respects cgroup CPU limits on Linux. Remove manual workarounds: ```go // Before: using uber-go/automaxprocs import _ "go.uber.org/automaxprocs" // After (Go 1.25+): built-in, remove the import // GOMAXPROCS is set automatically from cgroup CPU limits ``` ### `encoding/json/v2` (experimental) _(Go 1.25+, GOEXPERIMENT=jsonv2)_ Major JSON revision. **Experimental** — evaluate for new code, don't migrate production yet. ### Go 1.25 additions to prefer when target allows - `sync.WaitGroup.Go`: simple fire-and-wait goroutines; function must not panic; no errors/cancellation. - `testing/synctest.Test` and `synctest.Wait`: stable deterministic concurrent/time tests. Do not use the Go 1.24 experimental `synctest.Run` in Go 1.25+. - `net/http.CrossOriginProtection`: stdlib helper for cross-origin / CSRF-style protection in HTTP servers. - `reflect.TypeAssert[T](v)`: prefer over `v.Interface().(T)` in reflection code. - `os.Root.FS` and additional `os.Root` methods: use for confined filesystem APIs. - New vet checks: `waitgroup` misuse and manual host:port formatting; prefer `net.JoinHostPort`. --- ## Go 1.26 Modernizations (February 2026) Changelog: ### Use `errors.AsType[T]()` _(Go 1.26+)_ ```go // Before var pathErr *os.PathError if errors.As(err, &pathErr) { fmt.Println(pathErr.Path) } // After (Go 1.26+) if pathErr, ok := errors.AsType[*os.PathError](err); ok { fmt.Println(pathErr.Path) } ``` ### Use enhanced `new()` _(Go 1.26+)_ `new(expr)` now accepts a value expression and returns a pointer to it (not zero-initialized): ```go // Before: helper function needed func ptr[T any](v T) *T { return &v } cfg := Config{Timeout: ptr(30)} // After (Go 1.26+): new(expr) initializes the value — equivalent to ptr(30) cfg := Config{Timeout: new(30)} // *int pointing to 30, not 0 ``` ### Use `crypto/hpke` _(Go 1.26+)_ Hybrid Public Key Encryption (RFC 9180) is now in the standard library. ### Use RSA-OAEP or HPKE instead of new PKCS#1 v1.5 encryption _(Go 1.26+)_ For new encryption use, avoid `crypto/rsa.EncryptPKCS1v15`. Prefer RSA-OAEP (`rsa.EncryptOAEP` / `rsa.EncryptOAEPWithOptions`) or a modern KEM/HPKE design. ### Green Tea GC enabled by default _(Go 1.26+)_ Re-evaluate GC and allocation tuning under Go 1.26 Green Tea GC using profiles and benchmarks. Remove legacy tuning only when data supports it. Keep `GOMEMLIMIT` when it represents a real container or service memory ceiling. Remove third-party `automaxprocs` workarounds unless the project has a measured reason, because Go 1.25+ makes `GOMAXPROCS` container-aware by default. ### Go 1.26+ test artifacts Use `t.ArtifactDir()`, `b.ArtifactDir()`, and `f.ArtifactDir()` for files created by tests, benchmarks, and fuzzers that should persist for inspection. ### Go 1.26+ slog multi-handler For simple fan-out to multiple slog handlers, prefer stdlib `slog.NewMultiHandler` before adding third-party handler-composition dependencies. ### Go 1.26+ ReverseProxy For new reverse proxy code, prefer `httputil.ReverseProxy{Rewrite: ...}`. Do not generate new `Director`-based proxy code unless preserving old compatibility. ```go proxy := &httputil.ReverseProxy{ Rewrite: func(pr *httputil.ProxyRequest) { pr.SetURL(targetURL) pr.SetXForwarded() }, } ``` ### Small Go 1.26+ API preferences - Use `bytes.Buffer.Peek(n)` when you need to inspect upcoming bytes without consuming them. - Use reflect iterators where they simplify code: - `reflect.Type.Fields()` - `reflect.Type.Methods()` - `reflect.Type.Ins()` - `reflect.Type.Outs()` - `reflect.Value.Fields()` - `reflect.Value.Methods()` - Prefer these over manual `NumField`/`Field(i)` or `NumMethod`/`Method(i)` loops when the iterator form is clearer. ### Go 1.26+ goroutine leak profile For Go 1.26 diagnostics, there is an experimental goroutine leak profile. It is useful for production-oriented leak investigation, but is gated by `GOEXPERIMENT=goroutineleakprofile`; do not rely on it as default stable behavior. ### Go 1.26+ documentation command Use `go doc`, not `go tool doc`. Go 1.26 removed the old `cmd/doc` / `go tool doc` path. ### Go 1.26+ module target note When using a Go 1.26 or newer toolchain, `go mod init` may create a module with an older default `go` directive. If the project intentionally targets Go 1.26+ APIs, update the directive deliberately: ```bash go mod edit -go=1.26 go mod tidy ``` For future Go versions, use the project's intended target version. Do not use APIs newer than the module's `go` directive until the project explicitly agrees to upgrade it. ### Modernized `go fix` _(Go 1.26+)_ Go 1.26 rewrote `go fix` to apply a subset of modernize-style analyzers automatically. Check `go tool fix help` for exact coverage; some modernizations still require linting or manual review. ```bash go fix ./... # applies the enabled safe transformations ``` --- ## General Modernization (Any Version) ### Code MUST use `any` instead of `interface{}` _(Go 1.18+)_ ```go // Before func process(data interface{}) interface{} { ... } // After (Go 1.18+) func process(data any) any { ... } ``` ### Use generics instead of `interface{}` + type assertions _(Go 1.18+)_ ```go // Before func Contains(slice []interface{}, item interface{}) bool { ... } // After (Go 1.18+) func Contains[T comparable](slice []T, item T) bool { ... } // Or better (Go 1.21+): slices.Contains ``` ### Use `errors.Join` instead of multi-error libraries _(Go 1.20+)_ ```go // Before: hashicorp/go-multierror or uber-go/multierr errs = multierror.Append(errs, err1) return errs.ErrorOrNil() // After (Go 1.20+) return errors.Join(err1, err2) ``` ### Use `net.JoinHostPort` instead of `fmt.Sprintf` _(any version)_ ```go // Before (broken for IPv6) addr := fmt.Sprintf("%s:%d", host, port) // After (handles IPv6 correctly: [::1]:8080) addr := net.JoinHostPort(host, strconv.Itoa(port)) ```