19 KiB
Go Version Modernizations
Go 1.21 Modernizations (August 2023)
Changelog: https://go.dev/doc/go1.21
Use built-in min, max, clear (Go 1.21+)
Remove custom implementations. min/max work with any ordered type and accept variadic arguments:
// 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:
// 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.
// 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+)
// 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) })
// Before: manual search
found := false
for _, v := range items { if v == target { found = true; break } }
// After (Go 1.21+)
found := slices.Contains(items, target)
// Before: manual clone
clone := append([]string(nil), original...)
// After (Go 1.21+)
clone := slices.Clone(original)
Use maps package (Go 1.21+)
// 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+)
// 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+)
// 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+)
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: https://go.dev/doc/go1.22
SHOULD use range over integers (Go 1.22+)
// 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.
// 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+)
// 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)
Readremoved — usecrypto/randfor random bytes
Use enhanced net/http routing (Go 1.22+)
// 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+)
// 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+)
// Before
t := reflect.TypeOf((*MyInterface)(nil)).Elem()
// After (Go 1.22+)
t := reflect.TypeFor[MyInterface]()
Use database/sql.Null[T] (Go 1.22+)
// 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: https://go.dev/doc/go1.23
Use iterators (range over functions) (Go 1.23+)
Go 1.23 introduced range-over-func with the iter package:
// 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+)
// 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+)
// 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.Timerandtime.Tickerare garbage collected without callingStop()- 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: https://go.dev/doc/go1.24
Use generic type aliases (Go 1.24+)
// 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.
// 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:
// 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:
// 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+)
// 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+)
// 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+)
// 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+)
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:
// 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.
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.
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)
// 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: https://go.dev/doc/go1.25
Use sync.WaitGroup.Go (Go 1.25+)
// 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)
// 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:
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:
// 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.Testandsynctest.Wait: stable deterministic concurrent/time tests. Do not use the Go 1.24 experimentalsynctest.Runin Go 1.25+.net/http.CrossOriginProtection: stdlib helper for cross-origin / CSRF-style protection in HTTP servers.reflect.TypeAssert[T](v): prefer overv.Interface().(T)in reflection code.os.Root.FSand additionalos.Rootmethods: use for confined filesystem APIs.- New vet checks:
waitgroupmisuse and manual host:port formatting; prefernet.JoinHostPort.
Go 1.26 Modernizations (February 2026)
Changelog: https://go.dev/doc/go1.26
Use errors.AsType[T]() (Go 1.26+)
// 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):
// 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.
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)orNumMethod/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:
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.
go fix ./... # applies the enabled safe transformations
General Modernization (Any Version)
Code MUST use any instead of interface{} (Go 1.18+)
// 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+)
// 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+)
// 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)
// Before (broken for IPv6)
addr := fmt.Sprintf("%s:%d", host, port)
// After (handles IPv6 correctly: [::1]:8080)
addr := net.JoinHostPort(host, strconv.Itoa(port))