What’s New and Migrating Older Code

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Go 1.27 (go1.27.2), checked on 2026-10-18; every go.mod shown has the line go 1.27 unless the example is about an older language version. Go 1.28 is not released: it is expected in February 2027, no release candidate exists, and the go.dev/doc/go1.28 page is an empty draft, so it appears here only as "upcoming". The page has three jobs. First, a release-by-release record of what changed in the language, the tools, the runtime and the standard library from Go 1.18 to Go 1.27. Second, a practical guide to migrating older code, blog posts and tutorials to today’s idioms, with the real output of the commands that do most of the work (go vet, go fix, the godebug directive). Third, one table that collects every statement in the books this reference consulted that is outdated or wrong, with the version in which the situation changed.

It is the last page of the section on purpose: every topic page already has a short "where the books are outdated" passage, and this page gathers them. The version policy and the go and toolchain lines are the subject of Toolchain Versions and Compatibility; the tools used for migration are described in Formatting, vet, fix and Static Analysis. The idioms that replace the old ones are on Idioms, Style and Code Review and Common Mistakes and Gotchas. For first steps with the language see Getting Started.

Older code is shown here only in passages labelled Older code you may meet. It is shown so that you can recognise it and replace it; it is not a recommendation. All other pages of the section use the current forms. Every program and command below was run with go1.27.2 on darwin/arm64; compiler, vet and go fix messages are copied from real runs. Timings and random numbers are machine-specific, as noted where they appear. Unless a different go line is shown, the module is:

module example.com/shelf

go 1.27

How "since version" works

Three mechanisms decide whether a change applies to your code, and they are easy to confuse.

Mechanism What it controls Example

The toolchain (go version)

The compiler, linker, go command, runtime and standard library you run. New library functions and new tools appear with it.

slices.Concat exists in every toolchain from 1.22 on, whatever the module says.

The go line of go.mod (or //go:build go1.N in a file)

The language version of each package and the minimum toolchain. A new language feature is rejected below its version, and a few behavior changes depend on it.

min(3, 1, 2) fails to compile with go 1.20; per-iteration loop variables apply only with go 1.22 or later.

GODEBUG defaults, derived from the go line

Run-time behavior changes that could break programs. The go line selects the defaults that were current for that version, and a godebug directive or //go:debug comment overrides single settings.

httpmuxgo121=1 restores the old ServeMux; randseednop=0 makes rand.Seed work again.

The compiler enforces the language version. This is the real message for a builtin from 1.21 in a module that says go 1.20:

./m.go:3:16: built-in min requires go1.21 or later (-lang was set to go1.20; check go.mod)

Library symbols are not checked by the compiler, but since Go 1.27 go test runs the stdversion vet analyzer by default (go vet has run it since Go 1.23). With go 1.21 in go.mod and a call to slices.Concat, which arrived in Go 1.22, go vet reports:

main.go:9:16: slices.Concat requires go1.22 or later (module is go1.21)

The program still builds and runs on a newer toolchain; the report tells you the code would not build on the toolchain the go line promises. The analyzer only reasons about modules that declare go 1.21 or later, because earlier go lines were advisory.

Go 1.18 to Go 1.27 at a glance

Release Date Language and spec Tools and runtime headlines

1.18

2022-03-15

Type parameters, any, comparable, type sets with ~T and unions

Native fuzzing, go work workspaces, go get no longer builds or installs, GOAMD64 levels, slice growth smoothed

1.19

2022-08-02

Memory model revised (June 6, 2022), //go:build unix

GOMEMLIMIT, typed atomics, doc comment syntax, sort uses pattern-defeating quicksort

1.20

2023-02-01

Slice to array conversion, unsafe.SliceData/String/StringData, comparable satisfied by interfaces

errors.Join, PGO preview, coverage for binaries, math/rand auto-seeded, WithCancelCause

1.21

2023-08-08

min, max, clear, panic(nil) change, initialization order specified, better inference

slices, maps, cmp, log/slog, PGO GA, toolchain line and GOTOOLCHAIN, GODEBUG by go line, wasip1

1.22

2024-02-06

Per-iteration loop variables, range over integers

math/rand/v2, ServeMux patterns, go/version, new execution tracer, slices.Concat

1.23

2024-08-13

Range over functions (iter)

Timer rework, unique, structs.HostLayout, godebug directive, telemetry, maps and slices iterators

1.24

2025-02-11

Generic type aliases

tool directives, os.Root, weak, AddCleanup, b.Loop, Swiss-table maps, FIPS 140-3, omitzero, //go:wasmexport

1.25

2025-08-12

No language change; core types removed from the spec

Container-aware GOMAXPROCS, WaitGroup.Go, testing/synctest, flight recorder, ignore directive, CrossOriginProtection

1.26

2026-02-10

new(expr), self-referential constraints

Green Tea GC default, go fix rewritten with modernizers, errors.AsType, slog.NewMultiHandler, crypto/hpke

1.27

2026-08-19

Generic methods, promoted-field keys in struct literals, wider function type inference

encoding/json/v2 GA, uuid, crypto/mldsa, goroutine leak profile GA, size-specialized malloc, asynctimerchan removed

The next diagram shows only the language changes, the slowest-moving part: seven releases in ten changed the spec at all, and none of the changes broke an existing program.

timeline title Changes to the Go language, 1.18 to 1.27 2022-03 : Go 1.18 : type parameters : any and comparable : type sets and approximation elements 2022-08 : Go 1.19 : memory model revision : no language change 2023-02 : Go 1.20 : slice to array conversion : unsafe SliceData String StringData : any satisfies comparable 2023-08 : Go 1.21 : min max clear : panic with a nil value : package initialization order specified 2024-02 : Go 1.22 : per-iteration loop variables : range over integers 2024-08 : Go 1.23 : range over function types : iterators in the standard library 2025-02 : Go 1.24 : generic type aliases 2025-08 : Go 1.25 : no language change : core types removed from the spec text 2026-02 : Go 1.26 : new with an expression : self-referential type constraints 2026-08 : Go 1.27 : generic methods : promoted fields as struct literal keys : generalized function inference

The spec’s own appendix of language versions has no entry for Go 1.26 although new(expr) is described in the spec body, so "since" versions on this page come from the release notes, not from the spec.

Source: Release History

Release by release

The tables list what a working programmer is most likely to meet. Each release also fixed bugs, added ports and sharpened vet and the compiler; the linked release notes are the complete record. "GA" means generally available after an experiment or preview.

Go 1.18 (2022-03-15)

Area Change

Language

Type parameters on functions and types, the predeclared any and comparable, constraint interfaces with unions and ~T; a generic function can be instantiated explicitly or inferred. Several limits shipped at first (for example no type declarations inside generic functions) and were lifted later. See Generics.

Tools

Native fuzzing (go test -fuzz); go work workspaces (go.work); go get stops building and installing packages, which go install pkg@version does; the build embeds version control information (go version -m); GOAMD64 microarchitecture levels.

Runtime

Slice capacity growth uses a threshold of 256 and a smoother formula instead of doubling to 1024; the register ABI reaches arm64 and ppc64.

Library

strings.Cut and bytes.Cut, net/netip, sync.Mutex.TryLock, reflect.Pointer, debug/buildinfo.

Go 1.19 (2022-08-02)

Area Change

Language

No language change. The Memory Model was revised (still the version of June 6, 2022 today): sequentially consistent atomics are defined, and the model aligns with C, C++ and Rust. See Memory Model and Data Races.

Tools

//go:build unix constraint; doc comments gain links, lists and headings, and gofmt reformats them. See Documentation and Doc Comments.

Runtime

The soft memory limit GOMEMLIMIT (and debug.SetMemoryLimit); initial goroutine stack size adapts to the average; jump tables for large switch statements.

Library

Typed atomics (atomic.Int64, atomic.Pointer[T] and friends), sort switches to pattern-defeating quicksort, url.JoinPath, fmt.Append family, flag.TextVar, os/exec refuses relative-path results (ErrDot).

Go 1.20 (2023-02-01)

Area Change

Language

Conversion from a slice to an array (and to an array pointer since 1.17); unsafe.SliceData, unsafe.String, unsafe.StringData; an interface type that is not strictly comparable (such as any) now satisfies comparable; struct and array comparison is specified to stop at the first difference.

Tools

Profile-guided optimization preview; coverage profiles for whole programs (go build -cover, GOCOVERDIR); the standard library is no longer shipped precompiled; go test -skip.

Runtime

math/rand seeds its global generator randomly at start-up; rand.Seed is deprecated.

Library

errors.Join and several %w verbs in one fmt.Errorf; context.WithCancelCause; http.ResponseController; time.DateTime, DateOnly, TimeOnly and Time.Compare; strings.CutPrefix and CutSuffix; exec.Cmd.Cancel and WaitDelay; crypto/ecdh; the experimental arena package.

Go 1.21 (2023-08-08)

Area Change

Language

Builtins min, max and clear; panic(nil) now produces a *runtime.PanicNilError (GODEBUG=panicnil=1 restores the old result); package initialization order is specified precisely; type inference handles generic functions passed as arguments and untyped constants better.

Tools

The go line becomes a strict minimum, the toolchain line and GOTOOLCHAIN let the go command download the toolchain a module needs; GODEBUG defaults follow the go line; PGO GA (default.pgo is picked up automatically with -pgo=auto); the wasip1 port and //go:wasmimport; Windows 10 or later required.

Library

New packages log/slog, slices, maps, cmp, testing/slogtest; sync.OnceFunc, OnceValue and OnceValues; context.WithoutCancel, AfterFunc, WithDeadlineCause and WithTimeoutCause; errors.ErrUnsupported; runtime.Pinner; reflect.Value.Clear.

Go 1.22 (2024-02-06)

Area Change

Language

Each iteration of a for loop has its own copy of the loop variables, for modules that say go 1.22 or later; for i := range 10 ranges over an integer; range-over-func is available as GOEXPERIMENT=rangefunc only.

Tools

go/version; go work vendor; vet checks for time.Since in defer, append with a single argument, and slog argument mismatches; go test -cover reports packages without tests; vet stops reporting loop-variable captures in files that require Go 1.22.

Runtime

A redesigned execution tracer (lower overhead, streaming, per-thread buffers); metadata layout changes cut CPU overhead 1 to 3 percent; PGO enables inlining and devirtualization improvements.

Library

math/rand/v2 (and a ChaCha8 and PCG generator); ServeMux accepts methods and wildcards in patterns, with Request.PathValue; http.ServeFileFS, FileServerFS; slices.Concat, Insert always panics on a bad index, Delete/Compact/Replace zero the vacated tail; cmp.Or; database/sql.Null[T]; reflect.TypeFor.

Go 1.23 (2024-08-13)

Area Change

Language

Range over function types (func(yield func(K, V) bool)), with the standard iter.Seq and iter.Seq2 and iter.Pull. See Iterators and Range-over-Func.

Tools

The godebug directive in go.mod and //go:debug comments; Go telemetry (local by default); go mod tidy -diff; go env -changed; restrictions on //go:linkname pulls into the standard library; the stdversion vet analyzer.

Runtime

Timers and tickers that are no longer referenced become collectable even if not stopped, and their channels are unbuffered (synchronous) in modules that say go 1.23 or later; GODEBUG=asynctimerchan=1 could undo it until Go 1.27 removed the setting.

Library

unique (canonicalizing handles), structs.HostLayout, iterator forms in slices and maps (All, Values, Collect, Sorted, Chunk, Keys, Insert), slices.Repeat, os.CopyFS, sync.Map.Clear, path/filepath.Localize, reflect.Value.Seq.

Go 1.24 (2025-02-11)

Area Change

Language

Generic type aliases (type Set[T comparable] = map[T]struct{}).

Tools

tool directives in go.mod, go get -tool, go tool runs them, the tool meta-pattern; go build -json and structured go test -json build output; GOAUTH for private module fetches; the main module’s version is stamped into binaries (+dirty for uncommitted changes); GOCACHEPROG; cgo annotations #cgo noescape and #cgo nocallback; vet gains tests and a non-constant format string check.

Runtime

Swiss-table maps, a new internal mutex and cheaper small-object allocation (2 to 3 percent less CPU on average); runtime.AddCleanup.

Library

os.Root; testing.B.Loop, T.Context, T.Chdir; weak pointers; crypto/mlkem, crypto/hkdf, crypto/pbkdf2, crypto/sha3; the FIPS 140-3 module; omitzero for encoding/json; strings.Lines, SplitSeq, FieldsSeq; maphash.Comparable; rand.Text; //go:wasmexport and a WASI reactor mode; the experimental testing/synctest.

Go 1.25 (2025-08-12)

Area Change

Language

No change affecting programs. The notion of core types was removed from the spec in favor of dedicated prose for each operation.

Tools

ignore directive in go.mod; go doc -http; go version -m -json; the work package pattern; vet analyzers waitgroup (misplaced Add) and hostport (Sprintf("%s:%d") addresses); go build -asan leak detection; module roots in repository subdirectories; the go command stops adding a toolchain line when it updates the go line.

Runtime

GOMAXPROCS respects cgroup CPU limits and is updated at run time; the Green Tea collector as an experiment; the trace flight recorder; a nil-check bug introduced in 1.21 is fixed (programs that used a result before checking its error can now panic correctly); DWARF 5; unhandled re-panics print [recovered, repanicked].

Library

sync.WaitGroup.Go; testing/synctest GA; encoding/json/v2 experiment; http.CrossOriginProtection; T.Attr and T.Output; hash.Cloner; reflect.TypeAssert; more os.Root methods.

Go 1.26 (2026-02-10)

Area Change

Language

new takes an expression (new(yearsSince(born)) yields a *int holding the value); a generic type may refer to itself in its own type parameter list (type Adder[A Adder[A]] interface{ Add(A) A }).

Tools

go fix rewritten: it is now the home of the modernizers, shares the analysis framework with go vet, and supports //go:fix inline; go mod init is documented to write a lower go line (see Starting a module and the version in the go line for what go1.27.2 actually wrote); cmd/doc and go tool doc removed (use go doc); the pprof web UI opens on the flame graph.

Runtime

Green Tea GC enabled by default (10 to 40 percent less GC overhead in GC-heavy programs; GOEXPERIMENT=nogreenteagc opt-out, which the 1.26 notes said was expected to go in 1.27); cgo calls about 30 percent cheaper; randomized heap base address on 64-bit platforms; the goroutine leak profile as an experiment.

Library

errors.AsType[T]; slog.NewMultiHandler; crypto/hpke; bytes.Buffer.Peek; io.ReadAll about twice as fast with half the allocation; ServeMux trailing-slash redirects use status 307; T.ArtifactDir; reflect iterators (Type.Fields, Type.Methods, Value.Fields); os/signal.NotifyContext cancels with a cause naming the signal; testing/cryptotest; the experimental simd/archsimd and runtime/secret.

Go 1.27 (2026-08-19)

Area Change

Language

Generic methods (a method declaration may declare its own type parameters; interface methods may not, and generic methods cannot implement interface methods); a struct literal key may be any valid field selector, so promoted fields can be named; function type inference applies wherever a generic function is assigned or converted to a matching function type.

Tools

go test runs the stdversion vet check; go test -json output lines get an OutputType; go doc pkg@version and go doc -ex; new modernizers atomictypes, embedlit, slicesbackward, unsafefuncs, and waitgroup renamed waitgroupgo; go mod tidy merges duplicate require blocks into at most two for go 1.27 modules; gofmt alignment fix that can change whitespace in a few files; bzr support removed; response files (@file) for the compile, link, asm, cgo, cover and pack tools; go tool trace -http=:6060 listens on localhost only; vet reports wrapping a pointer to an error type with %w.

Runtime

Size-specialized allocation (up to 30 percent cheaper for small objects, about 60 KB of binary growth, GOEXPERIMENT=nosizespecializedmalloc opt-out expected to go in 1.28); the goroutine leak profile GA (goroutineleak, /debug/pprof/goroutineleak); tracebacks include pprof goroutine labels for go 1.27 modules; the asynctimerchan setting removed, so timer channels are always synchronous; macOS 13 or later required.

Library

encoding/json/v2 and encoding/json/jsontext GA with encoding/json reimplemented on top of v2 (opt-out GOEXPERIMENT=nojsonv2); uuid; crypto/mldsa; strings.CutLast and bytes.CutLast; Rand.N as a generic method in math/rand/v2; httptest.NewTestServer for synctest; url.URL.Clone; synctest.Sleep; maphash.Hasher; database/sql.ConvertAssign; Unicode 17; the experimental simd and simd/archsimd.

Go 1.28 (upcoming)

Go 1.28 is not released. The release notes at go.dev/doc/go1.28 are an empty stub on 2026-10-18, so nothing can be documented from them. Planning discussions mention a url.MustParse, an HTTP QUERY method and iterator methods in regexp and flag, but a feature is not part of Go until the release notes say so. When 1.28 ships, the opt-outs that the 1.27 notes call temporary (such as GOEXPERIMENT=nosizespecializedmalloc) are expected to disappear, and Go 1.26 leaves support.

A tour of the new features in one program

The program below uses one feature from each release 1.18 to 1.27 and prints a line per release. It needs a toolchain of 1.27 or later (the generic method and the uuid package are 1.27 features), so its go.mod says go 1.27.

package main

import (
	"errors"
	"fmt"
	"iter"
	"maps"
	"math/rand/v2"
	"slices"
	"strings"
	"sync"
	"sync/atomic"
	"uuid"
)

// Book is the running example from the shelf module.
type Book struct {
	Title string
	Pages int
}

// Set is a generic alias (1.24) for a map used as a set.
type Set[T comparable] = map[T]struct{}

// Shelf holds books.
type Shelf struct{ books []Book }

// Project is a generic method (1.27): the method declares its own type parameter.
func (s *Shelf) Project[T any](f func(Book) T) []T {
	out := make([]T, 0, len(s.books))
	for _, b := range s.books {
		out = append(out, f(b))
	}
	return out
}

// Pages is an iterator (1.23).
func (s *Shelf) Pages() iter.Seq[int] {
	return func(yield func(int) bool) {
		for _, b := range s.books {
			if !yield(b.Pages) {
				return
			}
		}
	}
}

// Map is a generic function (1.18).
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
}

type notFound struct{ title string }

func (e *notFound) Error() string { return "not found: " + e.title }

func main() {
	shelf := &Shelf{books: []Book{{"Dune", 612}, {"Emma", 474}, {"Ulysses", 730}}}

	// 1.18: generics, strings.Cut
	fmt.Println("1.18", Map(shelf.books, func(b Book) int { return len(b.Title) }))
	before, after, _ := strings.Cut("978-0141439518", "-")
	fmt.Println("1.18 cut:", before, after)

	// 1.19: typed atomics
	var loans atomic.Int64
	loans.Add(2)
	fmt.Println("1.19", loans.Load())

	// 1.20: errors.Join
	err := errors.Join(errors.New("title missing"), errors.New("isbn invalid"))
	fmt.Printf("1.20 %q\n", err.Error())

	// 1.21: min, max, clear, slices, maps
	fmt.Println("1.21", min(3, 1, 2), max(2.5, 1), slices.Max([]int{4, 9, 2}))
	m := map[string]int{"a": 1}
	clear(m)
	fmt.Println("1.21 cleared:", len(m))

	// 1.22: range over int and math/rand/v2 with an explicit seed
	r := rand.New(rand.NewPCG(1, 2))
	for i := range 3 {
		fmt.Print("1.22 ", i, " ", r.IntN(10), "; ")
	}
	fmt.Println()

	// 1.23: range over func, iterator helpers
	fmt.Println("1.23", slices.Collect(shelf.Pages()), slices.Sorted(maps.Keys(map[string]int{"b": 1, "a": 2})))

	// 1.24: generic alias, strings.SplitSeq
	s := Set[string]{"Dune": {}}
	for part := range strings.SplitSeq("a,b,c", ",") {
		fmt.Print(part, " ")
	}
	fmt.Println("1.24 set size", len(s))

	// 1.25: WaitGroup.Go
	var wg sync.WaitGroup
	var total atomic.Int64
	for _, b := range shelf.books {
		wg.Go(func() { total.Add(int64(b.Pages)) })
	}
	wg.Wait()
	fmt.Println("1.25 pages:", total.Load())

	// 1.26: new(expr) and errors.AsType
	pages := new(len(shelf.books))
	var wrapped error = fmt.Errorf("lookup: %w", &notFound{"Persuasion"})
	if nf, ok := errors.AsType[*notFound](wrapped); ok {
		fmt.Println("1.26", *pages, nf.title)
	}

	// 1.27: generic method, strings.CutLast, uuid
	fmt.Println("1.27", shelf.Project(func(b Book) string { return strings.ToUpper(b.Title) }))
	dir, file, _ := strings.CutLast("catalog/books/dune.json", "/")
	fmt.Println("1.27 cutlast:", dir, file)
	id, err := uuid.Parse("123e4567-e89b-42d3-a456-426614174000")
	fmt.Println("1.27 uuid:", id, err, len(uuid.New().String()))
}
1.18 [4 4 7]
1.18 cut: 978 0141439518
1.19 2
1.20 "title missing\nisbn invalid"
1.21 1 2.5 9
1.21 cleared: 0
1.22 0 7; 1.22 1 6; 1.22 2 7;
1.23 [612 474 730] [a b]
a b c 1.24 set size 1
1.25 pages: 1816
1.26 3 Persuasion
1.27 [DUNE EMMA ULYSSES]
1.27 cutlast: catalog/books dune.json
1.27 uuid: 123e4567-e89b-42d3-a456-426614174000 <nil> 36

The numbers drawn by the seeded generator are stable because rand.NewPCG(1, 2) is an explicit, reproducible source; the global functions of math/rand/v2 are not seedable at all. The pages of the topic each of these lines belongs to are Generics, Error Handling, Iterators and Range-over-Func, Sync and Atomics and Collections and Utility Packages.

Source: errors: AsType

Migrating older code: the plan

Migrating is mostly mechanical, and the toolchain does most of it. The order below keeps every step small enough to review and to revert.

Step Command or action Why this order

1

Commit a clean tree and run the tests with the toolchain you have now.

You need a green baseline to tell a migration problem from an old one.

2

Install the current toolchain (or let GOTOOLCHAIN=auto fetch it) and run the tests again without touching go.mod.

A new toolchain with an old go line keeps the old language rules and the old GODEBUG defaults, so this step isolates library and tool changes.

3

go vet ./...

Fix what vet reports first. The checks are cheap and go test runs a subset of them anyway.

4

Raise the go line (go get go@1.27 or edit go.mod) and run the tests.

This turns on the language and GODEBUG changes for your module: per-iteration loop variables, the new ServeMux, timers and the rest of the table in the GODEBUG section.

5

go fix -diff ./..., review, then go fix ./..., then gofmt -l . and the tests again.

The modernizers rewrite old idioms without changing behavior. Run it on a clean tree and commit the result on its own.

6

Replace the things no tool can rewrite: third-party routers, log calls, the b.N loops, header-cast unsafe code, golint in CI.

These need judgement; the sections below show the current form of each.

7

Remove godebug entries that were only a stopgap.

Each entry is a deferral, not a fix. go version -m shows the settings the binary was built with.

Starting a module and the version in the go line

go mod init writes the go line. The Go 1.26 release notes say it defaults to a lower version so that new modules stay usable with the supported releases: the toolchain 1.N.X writes go 1.(N-1).0. What go1.27.2 wrote in a fresh directory on 2026-10-18 was the toolchain’s own version:

$ go version
go version go1.27.2 darwin/arm64
$ mkdir shelf && cd shelf && go mod init example.com/shelf
go: creating new go.mod: module example.com/shelf
$ cat go.mod
module example.com/shelf

go 1.27.2

Check the line you get and set it deliberately; go mod edit -go=1.27 or go get go@1.27 changes it. A library that wants to stay usable with Go 1.26 can keep go 1.26: the language rules and GODEBUG defaults of that line then apply to the library’s own packages, and it cannot use generic methods or the other 1.27 library additions.

Older code you may meet: one file, four generations of habits

The file below collects habits from tutorials written between 2012 and 2021. It is the older code of this page: it compiles and runs on Go 1.27.2, which is the compatibility promise at work, but every part of it has a current form.

Older code you may meet:

// +build linux darwin

package main

import (
	"fmt"
	"io/ioutil"
	"math/rand"
	"os"
	"reflect"
	"sort"
	"sync"
	"unsafe"
)

// describe takes the old spelling of the empty interface.
func describe(v interface{}) string {
	return fmt.Sprintf("%T", v)
}

// view reinterprets a string as bytes through the old header types.
func view(s string) []byte {
	sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
	bh := reflect.SliceHeader{Data: sh.Data, Len: sh.Len, Cap: sh.Len}
	return *(*[]byte)(unsafe.Pointer(&bh))
}

func main() {
	dir, err := ioutil.TempDir("", "shelf")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer os.RemoveAll(dir)
	name := dir + "/titles.txt"
	if err := ioutil.WriteFile(name, []byte("Dune\nEmma\nUlysses\n"), 0o644); err != nil {
		fmt.Println(err)
		return
	}
	data, _ := ioutil.ReadFile(name)
	fmt.Printf("read %d bytes, view has %d bytes\n", len(data), len(view("Dune")))

	rand.Seed(42)
	fmt.Println("first draw:", rand.Intn(1000))

	titles := []string{"Dune", "Emma", "Ulysses"}
	var wg sync.WaitGroup
	var mu sync.Mutex
	var seen []string
	for _, t := range titles {
		t := t
		wg.Add(1)
		go func() {
			defer wg.Done()
			mu.Lock()
			seen = append(seen, t)
			mu.Unlock()
		}()
	}
	wg.Wait()
	sort.Strings(seen)
	fmt.Println(seen, describe(seen))
}

Run with go1.27.2 (the first draw number is different on every run, which is the first symptom):

read 18 bytes, view has 4 bytes
first draw: 240
[Dune Emma Ulysses] []string

The tools see some of the problems and not others. gofmt -l lists the file, because gofmt wants a //go:build line beside the old build constraint:

@@ -1,3 +1,4 @@
+//go:build linux || darwin
 // +build linux darwin

 package main

go vet reports the header-cast code, because it is a misuse of reflect.SliceHeader:

main.go:25:35: possible misuse of reflect.SliceHeader

The compiler reports nothing, and neither does go vet for io/ioutil or rand.Seed: both still work, and both are only deprecated. Deprecation shows up in editors through gopls and in staticcheck (check SA1019), not in the build. go fix -diff shows what it would change:

--- ./main.go (old)
+++ ./main.go (new)
@@ -1,10 +1,10 @@
+//go:build linux || darwin
 // +build linux darwin

 package main

 import (
 	"fmt"
-	"io/ioutil"
 	"math/rand"
 	"os"
 	"reflect"
@@ -14,7 +8,7 @@
 )

 // describe takes the old spelling of the empty interface.
-func describe(v interface{}) string {
+func describe(v any) string {
 	return fmt.Sprintf("%T", v)
 }

@@ -26,18 +20,18 @@
 }

 func main() {
-	dir, err := ioutil.TempDir("", "shelf")
+	dir, err := os.MkdirTemp("", "shelf")
 	if err != nil {
 		fmt.Println(err)
 		return
 	}
 	defer os.RemoveAll(dir)
 	name := dir + "/titles.txt"
-	if err := ioutil.WriteFile(name, []byte("Dune\nEmma\nUlysses\n"), 0o644); err != nil {
+	if err := os.WriteFile(name, []byte("Dune\nEmma\nUlysses\n"), 0o644); err != nil {
 		fmt.Println(err)
 		return
 	}
-	data, _ := ioutil.ReadFile(name)
+	data, _ := os.ReadFile(name)
 	fmt.Printf("read %d bytes, view has %d bytes\n", len(data), len(view("Dune")))

 	rand.Seed(42)
@@ -48,14 +33,11 @@
 	var mu sync.Mutex
 	var seen []string
 	for _, t := range titles {
-		t := t
-		wg.Add(1)
-		go func() {
-			defer wg.Done()
+		wg.Go(func() {
 			mu.Lock()
 			seen = append(seen, t)
 			mu.Unlock()
-		}()
+		})
 	}
 	wg.Wait()
 	sort.Strings(seen)

Read the diff as a list of the modernizers that fired: plusbuild and the //go:build fix, any, the os functions that replace io/ioutil, and waitgroupgo, which also removed the t := t copy because the module says go 1.27. Nothing touched rand.Seed or the header types, and the benchmark in the file’s test, which still counts to b.N, was not changed either; those need a person. After go fix ./... the first pass leaves the old // +build line behind, and a second go fix ./... removes it:

--- ./main.go (old)
+++ ./main.go (new)
@@ -1,5 +1,4 @@
 //go:build linux || darwin
-// +build linux darwin

 package main

After the two passes gofmt -l . prints nothing and go vet still reports only the header-cast function. The current form of that function, and of the rest of the file, is below. Only the parts that changed in meaning are commented.

//go:build unix

package main

import (
	"fmt"
	"math/rand/v2"
	"os"
	"slices"
	"sync"
	"unsafe"
)

// describe takes the modern spelling of the empty interface.
func describe(v any) string {
	return fmt.Sprintf("%T", v)
}

// view reinterprets a string as bytes without copying. The result must not
// be modified, because string data is read-only.
func view(s string) []byte {
	return unsafe.Slice(unsafe.StringData(s), len(s))
}

func main() {
	dir, err := os.MkdirTemp("", "shelf")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer os.RemoveAll(dir)
	name := dir + "/titles.txt"
	if err := os.WriteFile(name, []byte("Dune\nEmma\nUlysses\n"), 0o644); err != nil {
		fmt.Println(err)
		return
	}
	data, _ := os.ReadFile(name)
	fmt.Printf("read %d bytes, view has %d bytes\n", len(data), len(view("Dune")))

	// A reproducible generator is explicit; the global one is not seedable.
	r := rand.New(rand.NewPCG(42, 42))
	fmt.Println("first draw:", r.IntN(1000))

	titles := []string{"Dune", "Emma", "Ulysses"}
	var wg sync.WaitGroup
	var mu sync.Mutex
	var seen []string
	for _, t := range titles {
		wg.Go(func() {
			mu.Lock()
			seen = append(seen, t)
			mu.Unlock()
		})
	}
	wg.Wait()
	slices.Sort(seen)
	fmt.Println(seen, describe(seen))
}
read 18 bytes, view has 4 bytes
first draw: 619
[Dune Emma Ulysses] []string

The draw is reproducible only for a fixed seed pair, and 619 is what this pair produces on every run. In the older file the draw changed on every run, because from Go 1.24 rand.Seed does nothing in a module that says go 1.24 or later (the randseednop setting). To restore the seeding for a module you cannot change yet, set GODEBUG=randseednop=0; run twice, the older file then prints the same number both times:

$ GODEBUG=randseednop=0 go run .
first draw: 305
$ GODEBUG=randseednop=0 go run .
first draw: 305

The next sections take the habits one at a time. The first four were found in the file above.

Source: cmd/fix

Habit by habit: older form and current form

GOPATH to modules

Older code you may meet: code that lives under $GOPATH/src/<import path> and is built by go build with no go.mod. Go 1.11 introduced modules, Go 1.13 made the proxy and checksum database the default way to fetch them, and Go 1.16 made module mode the default. Inside a directory without go.mod, the real go1.27.2 result is:

$ go build .
go: go.mod file not found in current directory or any parent directory; see 'go help modules'

The old mode still exists as an explicit opt-out, for archaeology only. With GO111MODULE=off and a GOPATH that holds the package under src/, the program builds and runs:

$ GO111MODULE=off GOPATH=$PWD/../.. go run .
built in GOPATH mode

The current form is a module anywhere on disk, created with go mod init. GOPATH is now only the default location of the module cache ($GOPATH/pkg/mod) and of the binaries go install writes ($GOPATH/bin); on this machine go env GOPATH prints /Users/albertoirurueta/go. See Modules and Dependency Management and Workspaces, Versioning and Publishing for go work, which replaced the habit of keeping several repositories side by side under one GOPATH (Go 1.18).

go get to go install

Older code you may meet: go get github.com/someone/tool to download, build and install a command. Go 1.17 deprecated that use and Go 1.18 removed it; go get now only edits go.mod. Outside a module it fails with a message that names the replacement:

$ go get example.com/cmd/shelf
go: go.mod file not found in current directory or any parent directory.
	'go get' is no longer supported outside a module.
	To build and install a command, use 'go install' with a version,
	like 'go install example.com/cmd@latest'
	For more information, see https://go.dev/doc/go-get-install-deprecation
	or run 'go help get' or 'go help install'.

The current form is go install example.com/cmd/shelf@v1.2.3 (or @latest), which builds the command at that version, ignoring any go.mod in the current directory, and installs it in $GOBIN or $GOPATH/bin. With GOPROXY=off the lookup fails honestly:

$ go install example.com/cmd/shelf@v1.2.3
go: example.com/cmd/shelf@v1.2.3: module lookup disabled by GOPROXY=off

tools.go to tool directives

Older code you may meet: a file named tools.go that blank-imports the command packages a project depends on, behind a //go:build tools constraint, only so that go.mod records the versions. Go 1.24 added the tool directive: the module records the tool itself, go get -tool adds it, go tool name runs it (and caches the executable), and the tool meta-pattern means "every tool of this module", for example go get tool to upgrade them all or go install tool to install them. The go.mod form, written here with go mod edit -tool=... (no network is needed to record the directive):

module example.com/shelf

go 1.27

tool honnef.co/go/tools/cmd/staticcheck

Running go tool staticcheck then needs the module in go.mod (run go get -tool honnef.co/go/tools/cmd/staticcheck@version with network access). Without it, go1.27.2 explains exactly what is missing:

$ go tool staticcheck -version
no required module provides package honnef.co/go/tools/cmd/staticcheck; to add it:
	go get honnef.co/go/tools/cmd/staticcheck

go tool with no arguments also lists the tools that ship with the distribution (asm, cgo, compile and the rest), and since Go 1.25 fewer of them are prebuilt: the ones not needed by go build or go test are built the first time you run them. The tools.go file can be deleted once its imports are tool directives.

interface{} to any

Older code you may meet: interface{} as the empty interface, in every book and tutorial before Go 1.18. any is a predeclared alias for exactly that type (Go 1.18), so the two are identical and interchangeable, and any also reads better in constraints ([T any]). The any modernizer of go fix rewrites every use, which the diff above shows for describe. Where a single file must stay compilable with Go 1.17 or earlier, only the go line decides: the compiler rejects any for packages whose language version is below 1.18.

io/ioutil to os and io

Older code you may meet: the io/ioutil package, deprecated in Go 1.16 when its functions moved. It is frozen, not removed, so old code builds; the replacement is a one-to-one rename, and go fix performs it for the functions with a direct equivalent (the diff above changed TempDir, WriteFile and ReadFile).

Older function Current function Notes

ioutil.ReadAll

io.ReadAll

About twice as fast and allocating about half as much since Go 1.26

ioutil.ReadFile

os.ReadFile

Same behavior

ioutil.WriteFile

os.WriteFile

Same behavior

ioutil.ReadDir

os.ReadDir

Returns []fs.DirEntry (cheaper, no Stat per entry) instead of []fs.FileInfo, sorted by file name

ioutil.TempFile

os.CreateTemp

The pattern may contain a * that is replaced by the random part

ioutil.TempDir

os.MkdirTemp

Same pattern rule

ioutil.NopCloser

io.NopCloser

Same behavior

ioutil.Discard

io.Discard

Same behavior

See I/O, Files and the Filesystem for os.Root (Go 1.24), os.CopyFS and the io/fs abstractions that came with the replacements.

rand.Seed and math/rand/v2

Older code you may meet: rand.Seed(time.Now().UnixNano()) at the start of main, because both books and a decade of tutorials say the global generator of math/rand is deterministic. It was: before Go 1.20 an unseeded program produced the same numbers on every run. Since Go 1.20 the global generator is seeded randomly at start-up, rand.Seed is deprecated, and since Go 1.24 it is a no-op for modules that say go 1.24 or later (unless GODEBUG=randseednop=0). Go 1.22 added math/rand/v2, with cleaner names and better generators; a reproducible sequence now comes from an explicit source:

math/rand (older) math/rand/v2 (current)

rand.Intn(n)

rand.IntN(n)

rand.Int63n(n), rand.Int31n(n)

rand.Int64N(n), rand.Int32N(n)

rand.Int()

rand.Int() (non-negative int, same name)

rand.Seed(s) and the global generator

rand.New(rand.NewPCG(seed1, seed2)), or rand.NewChaCha8(seed) for a stronger stream

rand.New(rand.NewSource(s))

rand.New(rand.NewPCG(s, s))

time.Duration(rand.Int63n(int64(d)))

rand.N(d), a generic function (and, since Go 1.27, the method Rand.N)

rand.Perm(n), rand.Shuffle(n, swap)

Same names

Neither package is for secrets: crypto/rand is, and rand.Text (Go 1.24) generates a random string suitable for a token. The reasons for the changes are in Collections and Utility Packages and Crypto and Security.

Source: math/rand/v2

// +build to //go:build

Older code you may meet: build constraints written as // +build linux darwin (comma means AND, space means OR, and the line must be followed by a blank line). Go 1.17 introduced //go:build linux || darwin with real boolean operators; gofmt adds the new line next to an old one and keeps them in sync, vet’s buildtag analyzer checks them, and the plusbuild modernizer removes the old line (Go 1.16 and earlier were the last releases that needed it). The two-pass go fix run above is exactly this. Go 1.19 added the unix constraint, which stands for every Unix-like GOOS and shortens the common case:

//go:build unix

Constraints may also name a Go version (//go:build go1.22); vet since Go 1.24 rejects a point release such as go1.23.1 there. See Directives, Code Generation and Embedding.

b.N loops to b.Loop

Older code you may meet: a benchmark body written as for i := 0; i < b.N; i++ { ... }. It is not wrong, but the compiler may optimize away the work if the result is unused, the framework runs the function several times with growing b.N (so setup runs repeatedly), and forgetting the loop silently measures nothing. Go 1.24 added b.Loop(): the function runs once per -count, setup and cleanup outside the loop execute once, and the compiler keeps the loop body alive. The two versions side by side (no modernizer rewrites this; it is a manual change):

package benchloop

import (
	"strings"
	"testing"
)

var sink string

func BenchmarkJoinBN(b *testing.B) {
	parts := []string{"Dune", "Emma", "Ulysses"}
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		sink = strings.Join(parts, ", ")
	}
}

func BenchmarkJoinLoop(b *testing.B) {
	parts := []string{"Dune", "Emma", "Ulysses"}
	b.ReportAllocs()
	for b.Loop() {
		sink = strings.Join(parts, ", ")
	}
}
$ go test -bench . -benchtime=200000x
goos: darwin
goarch: arm64
pkg: example.com/shelf
cpu: Apple M5
BenchmarkJoinBN-10      	  200000	        53.47 ns/op	      24 B/op	       1 allocs/op
BenchmarkJoinLoop-10    	  200000	        51.86 ns/op	      24 B/op	       1 allocs/op
PASS
ok  	example.com/shelf	0.203s

The timings are machine-specific; the point is that the two forms agree here and that b.Loop is the one that stays correct when the body gets cheap enough for the compiler to remove it. -benchtime=Nx (a fixed count) has existed since Go 1.12. See Benchmarks, Fuzzing and synctest.

Loop-variable copies

Older code you may meet: v := v (or t := t) as the first line of a loop body, or the loop variable passed to a goroutine as an argument, to avoid the classic bug in which every closure sees the last value. Before Go 1.22 a for loop declared its variables once for the whole loop. Since Go 1.22, a module whose go line is 1.22 or later gets a new copy per iteration, and the copy is redundant. The same file, built under four go lines, shows that the language version of the module decides:

package main

import "fmt"

func main() {
	var show []func() int
	for i := 0; i < 3; i++ {
		show = append(show, func() int { return i })
	}
	for _, f := range show {
		fmt.Print(f(), " ")
	}
	fmt.Println()
}
go line in go.mod Output

go 1.20

3 3 3

go 1.21

3 3 3

go 1.22

0 1 2

go 1.27

0 1 2

There is no GODEBUG for this: the go line (or a //go:build go1.22 constraint in a file) is the switch, which also means that a library with go 1.21 in its go.mod keeps the old semantics for its own packages even when an application with go 1.27 imports it. The forvar modernizer removes the redundant copies. One consequence is a new vet warning in 1.24: a three-clause loop whose variable contains a sync.Locker copies the lock on every iteration. See Control Flow and Common Mistakes and Gotchas.

Third-party routers to ServeMux patterns

Older code you may meet: a router package added to a project only because the standard ServeMux matched fixed paths and prefixes and ignored the request method. Since Go 1.22 patterns can name a method, a path wildcard and a rest-of-path wildcard, and Request.PathValue reads the value. A method mismatch becomes a 405 automatically:

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /books/{isbn}", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "book %s\n", r.PathValue("isbn"))
	})
	mux.HandleFunc("GET /files/{path...}", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "file %s\n", r.PathValue("path"))
	})
	mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "home")
	})

	for _, c := range []struct{ method, target string }{
		{"GET", "/"},
		{"GET", "/books/9780141439518"},
		{"POST", "/books/9780141439518"},
		{"GET", "/files/a/b/c.txt"},
		{"GET", "/nothing"},
	} {
		rec := httptest.NewRecorder()
		mux.ServeHTTP(rec, httptest.NewRequest(c.method, c.target, nil))
		fmt.Printf("%-4s %-22s -> %d %q\n", c.method, c.target, rec.Code, rec.Body.String())
	}
}
GET  /                      -> 200 "home\n"
GET  /books/9780141439518   -> 200 "book 9780141439518\n"
POST /books/9780141439518   -> 405 "Method Not Allowed\n"
GET  /files/a/b/c.txt       -> 200 "file a/b/c.txt\n"
GET  /nothing               -> 404 "404 page not found\n"

The patterns {path...} (match the rest of the path) and {$} (match exactly the trailing slash) cover what routers added. Conflicting patterns panic at registration, not at request time. Go 1.26 changed the trailing-slash redirect of ServeMux to status 307, which keeps the method of a POST. A router is still a sensible choice when you need regular expressions, route groups or generated documentation; it is no longer needed for method and parameter routing. The old behavior is a GODEBUG away: with httpmuxgo121=1 the same GET /books/{isbn} pattern in the earlier example no longer matches as a wildcard, and the request returns 404 404 page not found. Full coverage is on net/http Clients and Servers.

log to slog

Older code you may meet: the log package with Printf-style messages that bake the fields into text, so a line like lent isbn=... member=... has to be parsed back into fields by whatever collects the logs. Go 1.21 added log/slog, with leveled, structured records and pluggable handlers (text, JSON, or your own), and keeps the old API working by routing it through the same handler:

package main

import (
	"log"
	"log/slog"
	"os"
)

func main() {
	// Older style: a formatted line, with the fields baked into the message.
	old := log.New(os.Stdout, "", 0)
	old.Printf("lent isbn=%s member=%d", "9780141439518", 42)

	// Current style: a message plus typed key/value attributes.
	opts := &slog.HandlerOptions{ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
		if a.Key == slog.TimeKey && len(groups) == 0 {
			return slog.Attr{} // drop the time so the output is stable
		}
		return a
	}}
	logger := slog.New(slog.NewTextHandler(os.Stdout, opts))
	logger.Info("lent", "isbn", "9780141439518", "member", 42)

	// Bridge: code that still calls log.Printf now flows through the slog handler.
	slog.SetDefault(logger)
	log.Printf("legacy call isbn=%s", "9780141439518")

	jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, opts))
	jsonLogger.Warn("overdue", slog.Group("loan", slog.String("isbn", "9780141439518"), slog.Int("days", 3)))
}
lent isbn=9780141439518 member=42
level=INFO msg=lent isbn=9780141439518 member=42
level=INFO msg="legacy call isbn=9780141439518"
{"level":"WARN","msg":"overdue","loan":{"isbn":"9780141439518","days":3}}

slog.SetDefault makes the top-level log functions write through the slog handler (at level INFO), so a codebase can migrate package by package. Go 1.22 added a vet check for mismatched key/value arguments, and Go 1.26 added slog.NewMultiHandler to fan one record out to several handlers. See Structured Logging with slog.

JSON v1 to v2

Older code you may meet: encoding/json only. In Go 1.27 the package is still there and still the default, but it is now implemented on top of encoding/json/v2, and the new package and its low-level companion encoding/json/jsontext are generally available (a Go 1.25 and 1.26 experiment before that; GOEXPERIMENT=nojsonv2 selects the original implementation and is expected to go away). The v2 defaults are stricter. This program runs the same inputs through both packages:

package main

import (
	"encoding/json"
	jsonv2 "encoding/json/v2"
	"fmt"
)

type Book struct {
	Title   string   `json:"title"`
	Authors []string `json:"authors"`
	Pages   int      `json:"pages,omitzero"`
}

func main() {
	b := Book{Title: "Dune"}

	v1, _ := json.Marshal(b)
	v2, _ := jsonv2.Marshal(b)
	fmt.Println("v1 nil slice:", string(v1))
	fmt.Println("v2 nil slice:", string(v2))

	// A key that differs only in case: v1 matches it loosely, v2 does not.
	in := []byte(`{"TITLE":"Emma","pages":474}`)
	var a, c Book
	err1 := json.Unmarshal(in, &a)
	err2 := jsonv2.Unmarshal(in, &c)
	fmt.Printf("v1: %+v err=%v\n", a, err1)
	fmt.Printf("v2: %+v err=%v\n", c, err2)

	// Duplicate member names: v1 keeps the last, v2 reports an error.
	dup := []byte(`{"title":"A","title":"B"}`)
	var d1, d2 Book
	e1 := json.Unmarshal(dup, &d1)
	e2 := jsonv2.Unmarshal(dup, &d2)
	fmt.Printf("v1: %q err=%v\n", d1.Title, e1)
	fmt.Printf("v2: %q err=%v\n", d2.Title, e2)

	// Opting back into the old nil-slice rendering while using the v2 API.
	v2old, _ := jsonv2.Marshal(b, jsonv2.FormatNilSliceAsNull(true))
	fmt.Println("v2 + option:", string(v2old))
}
v1 nil slice: {"title":"Dune","authors":null}
v2 nil slice: {"title":"Dune","authors":[]}
v1: {Title:Emma Authors:[] Pages:474} err=<nil>
v2: {Title: Authors:[] Pages:474} err=<nil>
v1: "B" err=<nil>
v2: "A" err=jsontext: duplicate object member name "title"
v2 + option: {"title":"Dune","authors":null}

Three differences show: a nil slice marshals as [] in v2 and null in v1, member names match case-sensitively in v2 (the TITLE key was ignored), and duplicate names are an error. v2 also rejects invalid UTF-8. Because v1 is now built on v2 with options that preserve its historical behavior, nothing has to change for existing code, but error message texts may differ. Migrating gradually is supported: pass v2 options to get v1 behavior one difference at a time, then drop them. The official migration guide lists all the differences and options. See encoding/json.

golint to vet, gopls and staticcheck

Older code you may meet: golint ./... in a Makefile or CI job, because both books (and years of style guides) recommend it. golint was frozen in 2021, later archived, and is not maintained. What replaces it is a set of layers, none of which needs the old tool:

Layer What it provides

gofmt, go vet

Formatting and the correctness analyzers; go test runs a subset of vet before every test run. New analyzers arrive every release (tests, waitgroup, hostport, stdversion, the %w pointer check of 1.27).

gopls

The language server behind VS Code, GoLand, Vim and Emacs; it runs the same analyzers plus quick fixes and the modernizers, and organizes imports on save.

go fix

Applies the modernizers (Go 1.26), including the source-level inliner for your own //go:fix inline migrations.

staticcheck and golangci-lint

Community tools: deprecation (SA1019), bug patterns and style. The configuration format of golangci-lint changed in its version 2, so a pre-2 config needs migrating. Neither is part of the Go distribution.

go tool with a tool directive

Pins the version of a community tool in go.mod so every developer and CI run the same one.

The style checks that golint made (comment on exported names, no unneeded else after return) now live in gopls and in the community linters; oracle and guru, mentioned alongside it in older books, were replaced by gopls, and the godoc binary by go doc. See Formatting, vet, fix and Static Analysis.

Source: gopls

Header-cast unsafe code to unsafe.String, StringData, Slice and SliceData

Older code you may meet: converting between strings and byte slices by casting the addresses of reflect.StringHeader and reflect.SliceHeader values (Bodner, chapter 14). Both header types are deprecated, and the technique is fragile: the garbage collector may not see the data pointer as a pointer, which is the vet message shown above. The Go 1.17 and 1.20 functions unsafe.Slice, unsafe.SliceData, unsafe.String and unsafe.StringData express the same operations with checked types, as view did in the current file above. The result of viewing a string as []byte must still be treated as read-only. The unsafefuncs modernizer (Go 1.27) rewrites pointer arithmetic on unsafe.Pointer to these functions where it can. See unsafe and Low-Level Programming.

Source: unsafe

Smaller replacements

Older habit Current form Since

sort.Ints, sort.Strings, sort.Slice

slices.Sort, slices.SortFunc (and the slicessort modernizer)

1.21

A loop that searches a slice for a value

slices.Contains, slices.Index (the slicescontains modernizer)

1.21

errors.As(err, &target) with a declared variable

errors.AsType[*MyErr](err)

1.26

strings.Split in a for range

strings.SplitSeq (the stringsseq modernizer)

1.24

strings.HasPrefix followed by strings.TrimPrefix

strings.CutPrefix (the stringscutprefix modernizer)

1.20

strings.Index and slicing around a separator

strings.Cut, strings.CutLast

1.18, 1.27

wg.Add(1), go func() { defer wg.Done() ... }()

wg.Go(func() { ... }) (the waitgroupgo modernizer)

1.25

if a < b { x = a } else { x = b }

min(a, b) (the minmax modernizer)

1.21

A loop that deletes all keys of a map

clear(m)

1.21

Helper func ptr[T any](v T) *T

new(expr), as in new(len(shelf.books))

1.26

for i := 0; i < n; i++ over a count

for i := range n (the rangeint modernizer)

1.22

reflect.TypeOf((*T)(nil)).Elem()

reflect.TypeFor[T]() (the reflecttypefor modernizer)

1.22

atomic.AddInt64(&n, 1) on a plain integer

atomic.Int64 fields (the atomictypes modernizer)

1.19

Transport.Dial

Transport.DialContext

1.7

context.WithCancel in a test

t.Context() (the testingcontext modernizer)

1.24

Stopping and draining a timer channel before Reset

Plain Reset; timer channels are synchronous

1.23, setting removed 1.27

Relying on panic(nil) recovering as nil

Check for *runtime.PanicNilError; recover() never returns nil for a panic

1.21

os.Open on user-controlled paths

os.Root, which refuses to leave its directory

1.24

Len/At style index loops

Iterator methods and iter.Seq (the stditerators modernizer)

1.23

The panic(nil) change deserves a run, because it changes what recover returns. With go 1.27:

package main

import (
	"fmt"
	"runtime"
)

func main() {
	defer func() {
		r := recover()
		_, isNil := r.(*runtime.PanicNilError)
		fmt.Printf("recovered %T, PanicNilError: %v\n", r, isNil)
		fmt.Println("message:", r)
	}()
	panic(nil)
}
recovered *runtime.PanicNilError, PanicNilError: true
message: runtime error: panic called with nil argument

The same program in a module that says go 1.20 prints recovered <nil>: <nil> (adjusting the format for the type), because the go line selects the old panicnil default. Slice capacity growth changed in Go 1.18 and is not controlled by any setting; appending 5000 integers one at a time to a nil slice reports these capacities on go1.27.2:

4 8 16 32 64 128 256 512 848 1280 1792 2560 3408 5120

The pre-1.18 rule doubled the capacity up to 1024 elements. The current rule switches to gradual growth after 256 elements, visible in the steps 512, 848, 1280, 1792, 2560 and beyond, and rounds each request to an allocator size class (the first append on this toolchain already allocates room for four integers). See Arrays and Slices.

The go fix modernizers

Go 1.26 rewrote go fix (the old fixers were all obsolete and are gone). It builds on the analysis framework of go vet, so the same analyzers either report a problem (go vet) or offer a safe rewrite (go fix). The suite of go1.27.2, as go tool fix help prints it:

Analyzer What it rewrites

any

interface{} to any

atomictypes

atomic.AddInt64(&x, ...) and friends on plain integers to the typed atomic.Int64

buildtag

checks //go:build and old build-constraint lines

embedlit

simplifies references to embedded fields in composite literals

errorsastype

errors.As to errors.AsType[T]

forvar

removes the redundant re-declaration of loop variables

hostport

checks addresses passed to net.Dial (reports Sprintf("%s:%d"))

inline

applies //go:fix inline directives

mapsloop

loops over maps to calls to the maps package

minmax

if/else statements to min and max

newexpr

helper functions that take the address of a value to new(expr)

omitzero

suggests omitzero for struct fields tagged omitempty

plusbuild

removes obsolete old-style build comments

rangeint

three-clause counting loops to for range n

reflecttypefor

reflect.TypeOf(x) to reflect.TypeFor[T]()

slicesbackward

backward loops over slices to slices.Backward

slicescontains

search loops to slices.Contains and ContainsFunc

slicessort

sort.Slice to slices.Sort for basic types

stditerators

Len/At style APIs to iterators

stringsbuilder

+= string accumulation to strings.Builder

stringscut

strings.Index plus slicing to strings.Cut

stringscutprefix

HasPrefix plus TrimPrefix to CutPrefix

stringsseq

ranging over Split and Fields to SplitSeq and FieldsSeq

testingcontext

context.WithCancel in tests to t.Context()

unsafefuncs

unsafe pointer arithmetic to function calls

waitgroupgo

wg.Add(1)/go/wg.Done() to wg.Go

The suite of Go 1.26 was the initial one; Go 1.27 added atomictypes, embedlit, slicesbackward and unsafefuncs, and renamed waitgroup to waitgroupgo; the 1.27 notes also say the fmtappendf analyzer was removed for stylistic reasons. buildtag and hostport are also go vet checks. The rewrites only apply where the module’s go line permits the new API, so raise the line first. Select analyzers with -name flags (for example go fix -rangeint ./...), preview with -diff, and keep the change in its own commit. On a file with several loops, slices searches and string splitting, the real output of go fix -diff is:

package main

import (
	"fmt"
	"sort"
	"strings"
)

// Deprecated: use Title.
//
//go:fix inline
func OldTitle(s string) string { return Title(s) }

// Title upper-cases the first letter of s.
func Title(s string) string {
	if s == "" {
		return s
	}
	return strings.ToUpper(s[:1]) + s[1:]
}

func contains(titles []string, want string) bool {
	for _, t := range titles {
		if t == want {
			return true
		}
	}
	return false
}

func clamp(n, lo, hi int) int {
	if n < lo {
		return lo
	}
	if n > hi {
		return hi
	}
	return n
}

func main() {
	titles := []string{"ulysses", "dune", "emma"}
	sort.Strings(titles)
	for i := 0; i < len(titles); i++ {
		titles[i] = OldTitle(titles[i])
	}
	for _, line := range strings.Split("a,b,c", ",") {
		fmt.Print(line, " ")
	}
	fmt.Println(contains(titles, "Dune"), clamp(12, 0, 10), titles)
	if strings.HasPrefix("978-0141439518", "978-") {
		fmt.Println(strings.TrimPrefix("978-0141439518", "978-"))
	}
}
--- ./main.go (old)
+++ ./main.go (new)
@@ -2,6 +2,7 @@

 import (
 	"fmt"
+	"slices"
 	"sort"
 	"strings"
 )
@@ -20,12 +21,7 @@
 }

 func contains(titles []string, want string) bool {
-	for _, t := range titles {
-		if t == want {
-			return true
-		}
-	}
-	return false
+	return slices.Contains(titles, want)
 }

 func clamp(n, lo, hi int) int {
@@ -41,14 +37,14 @@
 func main() {
 	titles := []string{"ulysses", "dune", "emma"}
 	sort.Strings(titles)
-	for i := 0; i < len(titles); i++ {
-		titles[i] = OldTitle(titles[i])
+	for i := range titles {
+		titles[i] = Title(titles[i])
 	}
-	for _, line := range strings.Split("a,b,c", ",") {
+	for line := range strings.SplitSeq("a,b,c", ",") {
 		fmt.Print(line, " ")
 	}
 	fmt.Println(contains(titles, "Dune"), clamp(12, 0, 10), titles)
-	if strings.HasPrefix("978-0141439518", "978-") {
-		fmt.Println(strings.TrimPrefix("978-0141439518", "978-"))
+	if after, ok := strings.CutPrefix("978-0141439518", "978-"); ok {
+		fmt.Println(after)
 	}
 }

The program printed the same two lines before and after (a b c true 10 [Dune Emma Ulysses] and 0141439518), as a modernizer must guarantee. Two spots were left alone, which is also informative: clamp, written with two early returns, and sort.Strings. go fix rewrites only the patterns its analyzers recognize and stays silent otherwise. The OldTitle call was replaced by the call it wraps, Title, because of the //go:fix inline directive on its declaration: that is how a library author ships a migration for callers, with a Deprecated: doc comment alongside.

GODEBUG settings that preserve old behavior

A change to run-time behavior that could break a working program ships with a GODEBUG setting, so that the new toolchain can keep behaving the old way. The rules (Go 1.21 and later) are:

  • A setting that was added in Go 1.N has, for each Go version, a default; the go line of the main module picks the defaults of that version. A go 1.20 module on a 1.27 toolchain behaves like Go 1.20 for every setting that exists.

  • A project overrides single settings in three places: a godebug directive in go.mod (Go 1.23), a //go:debug comment in a package main source file, or the GODEBUG environment variable at run time (which wins).

  • go version -m binary prints the DefaultGODEBUG the binary was built with, which is how you audit a deployed program.

  • Settings are kept for at least two years (four releases) and some for much longer; the policy permits removing them later. Since Go 1.27 the go command refuses a removed setting only if it is set to an old value, so programs that set a removed setting to its final default keep building.

A go.mod that keeps the pre-1.22 ServeMux while the rest of the module moves on:

module example.com/shelf

go 1.27

godebug httpmuxgo121=1
$ go build -o muxbin . && go version -m muxbin | grep -i godebug
	build	DefaultGODEBUG=httpmuxgo121=1

The go 1.27 line contributes no old defaults, so only the one explicit setting is listed. The same toolchain building a module that says go 1.21 reports many more, because every setting added after Go 1.21 defaults to its old value for that module:

$ go build -o app . && go version -m app | grep -o 'httplaxcontentlength=1,httpmuxgo121=1\|randseednop=0'
httplaxcontentlength=1,httpmuxgo121=1
randseednop=0

A package main can carry the same override without touching go.mod:

//go:debug randseednop=0
package main

The settings most often met while migrating, from the toolchain’s own table (Changed is the release in which the default changed; Old is the value that restores the earlier behavior):

Setting Changed Old value What the old value restores

panicnil

1.21

1

panic(nil) can be recovered as nil instead of *runtime.PanicNilError

httpmuxgo121

1.22

1

The pre-1.22 ServeMux: no methods or wildcards in patterns

httplaxcontentlength

1.22

1

Lenient handling of an invalid Content-Length header

httpservecontentkeepheaders

1.23

1

Keeps some headers on error responses from ServeContent and friends

winsymlink, winreadlinkvolume

1.23

0

Older Windows symbolic-link and Readlink behavior

x509negativeserial

1.23

1

Allows certificates with negative serial numbers

randseednop

1.24

0

rand.Seed seeds the global math/rand generator again

rsa1024min

1.24

0

Allows RSA keys under 1024 bits

multipathtcp

1.24

0

Multipath TCP is not used by default for listeners

httpcookiemaxnum, urlmaxqueryparams

1.24

0

No limit on the number of cookies or query parameters (the limits were backported as security fixes)

gotestjsonbuildtext

1.24

1

Text rather than JSON build output in go test -json

x509rsacrt, x509usepolicies

1.24

0

Older certificate and policy handling

containermaxprocs, updatemaxprocs

1.25

0

GOMAXPROCS ignores cgroup limits and is not updated at run time

tlssha1

1.25

1

Allows SHA-1 signature algorithms in TLS 1.2

x509sha256skid

1.25

0

SHA-1 based subject key identifiers

decoratemappings

1.25

0

No [anon: Go: ...] names on Linux memory mappings

urlstrictcolons

1.26

0

Accepts hostnames with colons outside brackets

httpservecontentmaxranges

1.26

1

No limit on the number of ranges in ServeContent

cryptocustomrand

1.26

1

crypto APIs use the caller-supplied random reader

tlsmlkem, tlssecpmlkem

1.24, 1.26

0

No hybrid post-quantum key exchange in the default TLS configuration

tracebacklabels

1.27

0

No goroutine labels in tracebacks

x509sslcertoverrideplatform

1.27

0

SSL_CERT_FILE and SSL_CERT_DIR ignored on Windows and macOS

Settings with no go-line history, such as netdns, http2client and http2server, are switches that were always settable at run time. The complete list, with the version in which each default changed, is the toolchain’s table and the go.dev/doc/godebug page.

Settings that were removed

A setting is removed when keeping it costs more than it is worth. The permanently removed ones are:

Setting Removed Note

x509sha1, tlskyber

1.24

Certificates signed with SHA-1 are rejected; the Kyber draft key exchange is gone (the tlskyber removal was documented in the 1.27 notes)

asynctimerchan

1.27

Timer and ticker channels are always synchronous and unbuffered, in every module

gotypesalias

1.27

go/types always produces Alias nodes

tlsunsafeekm, tlsrsakex, tls3des, tls10server, x509keypairleaf

1.27

Insecure TLS options that older modules could re-enable

A removed setting set to its final default is accepted; set to an old value it stops the build. Here a go 1.27 go.mod carries godebug asynctimerchan=1:

$ go build .
go: error loading go.mod:
go.mod:5: removed GODEBUG "asynctimerchan" set to old value "1" (https://go.dev/doc/godebug#go-127)

The effect on code: the timer program below prints the same result in a module that says go 1.22 and one that says go 1.27, because the asynchronous channel of old modules no longer exists:

package main

import (
	"fmt"
	"time"
)

func main() {
	t := time.NewTimer(10 * time.Millisecond)
	time.Sleep(50 * time.Millisecond)
	fmt.Println("len:", len(t.C), "cap:", cap(t.C))
	fmt.Println("Stop reports:", t.Stop())
}
len: 0 cap: 0
Stop reports: true

Before Go 1.23 the channel had a buffer of one and the same program printed len: 1 cap: 1 and Stop reports: false; code that drained the channel after Stop or tested len(t.C) to see whether a timer had fired must now use the return value of Stop and Reset instead. See Time and Timers.

Where the books are outdated: every statement in one table

This reference took its structure and narrative from two books, both of which describe an older Go:

  • Jon Bodner, Learning Go: An Idiomatic Approach to Real-World Go Programming, 1st edition (O’Reilly, March 2021), written for Go 1.15, with a look at the then draft generics design. A 2nd edition exists (O’Reilly, January 2024) and was not consulted.

  • Vladimir Vivien, Learning Go Programming (Packt, October 2016), written for Go 1.6 and 1.7 and the GOPATH workflow.

Neither text, listings nor figures are copied from either book; the table paraphrases each outdated statement in this page’s words. Where the table says "unchanged", the book is still right. The current behavior is verified against the Go 1.27 documentation, which wins on any discrepancy. The "Version" column is the release in which the situation changed.

Tooling, workspace and installation

Book Chapter What the book says (paraphrased) Current behavior Version

Vivien

1, 6

Code lives in a GOPATH workspace under src, and the build tools find packages there.

Modules: a go.mod in any directory; GOPATH is a cache and install location. GO111MODULE=off is an opt-out.

1.11, default 1.16

Vivien, Bodner

1, 6; 1

go get downloads, builds and installs a program.

go install pkg@version installs; go get edits go.mod only.

1.17, 1.18

Bodner

1

Install further Go versions with go get of the golang.org/dl wrapper programs and run them by versioned name.

The go line is a strict minimum and the toolchain line plus GOTOOLCHAIN=auto fetch the toolchain a module needs.

1.21

Both

1

Fetch dependencies straight from version control hosts.

Module proxy and checksum database by default (GOPROXY, GOSUMDB); GOPRIVATE for private code.

1.13

Both

various

Keep tool dependencies in a blank-import tools.go.

tool directives, go get -tool, go tool.

1.24

Bodner

9

Describes go.mod, semantic import versioning and vendoring for Go 1.15; no multi-module development.

Adds go work workspaces, the ignore directive and godebug/tool directives; bzr support is removed.

1.18, 1.25, 1.27

Bodner

1

Install goimports as a separate tool and lint with golint.

gopls organizes imports; go vet, gopls analyzers, staticcheck or golangci-lint replace golint, which is archived.

current

Vivien

1

The godoc binary serves documentation; oracle and guru navigate code.

go doc (with -http since 1.25 and pkg@version, -ex since 1.27) and pkg.go.dev; gopls replaces oracle and guru.

1.25, 1.27

Both

1

Format and vet are separate habits; doc comments are plain text.

go test runs a vet subset; doc comments have links, lists and headings; go fix modernizers apply idioms.

1.10, 1.19, 1.26

Vivien

1

The Playground has a fixed Go version and no network.

It has a fake network stack and file system (the fake clock still starts in 2009).

current

Vivien

1

Lists platform minimums such as Windows XP and OS X 10.7.

Windows 10 or Server 2016 (1.21), macOS 13 (1.27), Linux kernel 3.2 (1.24).

1.21, 1.24, 1.27

Bodner

1

Drive builds with Makefiles.

go build ./..., go run ./cmd/x, the build cache and go tool cover most of it; Makefiles remain fine for orchestration.

current

Both

various

Tool output is from Go 1.6 to 1.16.

Compiler and vet messages have been reworded over time.

current

Language and types

Book Chapter What the book says (paraphrased) Current behavior Version

Vivien

4

int and uint are 32 bits.

They are 32 or 64 bits depending on the platform; 64 on every 64-bit port since Go 1.1. Use fixed-size types when the width matters.

Go 1.1

Vivien

2

Every Go type can be compared for equality.

Slices, maps and functions can be compared only to nil; structs containing them are not comparable.

always

Vivien

7

A string is backed by an array of runes.

A string is a read-only byte sequence, usually UTF-8; a rune is a Unicode code point and the type of a character literal.

always

Vivien

4

A rune stores UTF-8.

A rune is an int32 code point; UTF-8 is the encoding of strings.

always

Vivien

2

Untyped constants are stored with unlimited precision.

Arbitrary precision exists only at compile time; a constant must be representable in the type it ends up with.

always

Vivien

5

A function type is identified by its parameter list.

Parameter types, result types and variadic-ness are all part of the type.

always

Vivien

1

Stringer is a built-in interface.

It is fmt.Stringer; the predeclared interfaces are error, any and comparable.

1.18 (any, comparable)

Vivien

4

Complete literal list omits 0b, 0o, hex floats and _ separators.

Binary and octal prefixes, hexadecimal floats and digit separators are in the language.

1.13

Vivien

8

An interface may not embed interfaces that share a method name.

Overlapping method sets are allowed when the signatures are identical.

1.14

Both

various

Shift counts must be unsigned.

Signed shift counts are allowed (a negative count panics at run time).

1.13

Bodner

3

go vet blocks the integer-to-string conversion string(i).

Vet reports it as a diagnostic; it is not a compile error. Use strconv.Itoa or string(rune(i)).

vet since 1.15

Bodner

3

Slice capacity doubles until 1024 elements.

Threshold 256 and a smoother formula.

1.18

Bodner

3

Copying slices and arrays needs loops; slice to array conversions do not exist.

slices.Clone, slices.Insert, slices.Delete, slices.Contains; conversion to an array pointer (1.17) and an array value (1.20); clear, min, max builtins.

1.17, 1.20, 1.21

Vivien

7

copy returns a deep copy with a new underlying array.

copy copies min(len(dst), len(src)) elements shallowly into an existing destination and allocates nothing; slices.Clone allocates and is also shallow.

always

Bodner

6

Take a pointer to a literal with a helper function or a temporary variable.

new(expr).

1.26

Both

various

Struct literal keys must be top-level fields.

Any valid field selector, including promoted fields, may be used as a key.

1.27

Both

7; 8

Method sets and embedding on non-generic types only.

Generic types (1.18) and generic methods (1.27); self-referential constraints (1.26).

1.18, 1.26, 1.27

Vivien

1

Calls the empty interface "generic".

Type parameters, type sets, ~T and unions are generics; interface{} or any is just the empty interface.

1.18

Bodner

15

Generics through the draft design with "type lists" and a special Go2Go playground.

Final syntax with unions, ~T, cmp.Ordered and comparable; better inference (1.21); generic type aliases (1.24); generic methods (1.27). The draft syntax never shipped.

1.18, 1.21, 1.24, 1.27

Bodner

15

User-defined containers cannot be ranged over.

Range over integers (1.22) and over functions with iter.Seq, iter.Seq2 and iter.Pull (1.23); slices and maps iterator helpers; strings and bytes iterators (1.24).

1.22, 1.23, 1.24

Both

4, 10; 3, 9

Copy the loop variable (v := v) or pass it to the goroutine to avoid every closure seeing the last value.

A new copy per iteration; the copies are redundant.

1.22

Both

2

Quote the unused-variable error as x declared but not used.

Go 1.27 prints declared and not used: x; the rule is unchanged.

reworded

Errors, concurrency and the runtime

Book Chapter What the book says (paraphrased) Current behavior Version

Vivien

1, 5

Compare sentinel errors with ==; inspect errors with type assertions.

Wrap with %w and use errors.Is, errors.As, errors.Unwrap; errors.Join and several %w verbs (1.20); errors.AsType[T] (1.26).

1.13, 1.20, 1.26

Both (era)

various

recover returned nil for panic(nil), so a nil result was read as "no panic".

panic(nil) produces *runtime.PanicNilError, so recover never returns nil for a real panic.

1.21

Bodner

8

Convert a panic to an error at a library boundary with recover.

Still sound, with two additions: panic(nil) now recovers as *runtime.PanicNilError, and an unhandled re-panic prints [recovered, repanicked].

1.21, 1.25

Vivien

9

The scheduler is cooperative: goroutines switch only at function calls and blocking operations.

Asynchronous preemption through signals.

1.14

Vivien

9

GOMAXPROCS defaults to the number of CPUs.

It also follows cgroup CPU limits on Linux and is updated at run time.

1.25

Vivien

9

Cancels goroutines with done channels, because context did not exist yet.

context is in the standard library (1.7); WithCancelCause (1.20); AfterFunc, WithoutCancel, WithDeadlineCause, WithTimeoutCause (1.21).

1.7, 1.20, 1.21

Bodner

12

Teaches context as of Go 1.15 (cancel, timeout, deadline, values).

Still right; the cause variants, AfterFunc and WithoutCancel came later.

1.20, 1.21

Bodner

10

Use sync/atomic functions on plain integers; sync.Once for one-time work.

Typed atomics (1.19), OnceFunc, OnceValue, OnceValues (1.21).

1.19, 1.21

Both

10; 9

Launch goroutines with wg.Add(1) and a deferred wg.Done().

sync.WaitGroup.Go.

1.25

Both

10; 9

Drain a timer’s channel before Reset, and expect time.After to leak in loops.

Unreferenced timers are collectable and channels are synchronous, so Stop and Reset never leave a stale value; the time.After leak is gone.

1.23 (setting removed 1.27)

Vivien

9

time.After closes its channel.

It sends a single value and never closes.

always

Both

10; 9

Find concurrency bugs with -race and goroutine dumps; the formal memory model is not covered.

The revised Memory Model (1.19, sequentially consistent atomics), testing/synctest (GA 1.25) and the goroutine leak profile (GA 1.27).

1.19, 1.25, 1.27

Both

6; 10

Tune the collector with GOGC only; the slice growth and map implementation are as the book describes.

GOMEMLIMIT (1.19), Swiss-table maps (1.24), Green Tea GC default (1.26), size-specialized malloc (1.27).

1.19, 1.24, 1.26, 1.27

Bodner

14

Convert between strings and byte slices with the header structs of reflect.

Those header types are deprecated; use unsafe.String, StringData, Slice and SliceData.

1.17, 1.20

Bodner

14

Cgo is for integration, not performance, and cgo calls are slow.

That advice stands; the cost of a call fell about 30 percent in 1.26, and #cgo noescape and nocallback (1.24) help the compiler.

1.24, 1.26

Standard library

Book Chapter What the book says (paraphrased) Current behavior Version

Both

various

Use io/ioutil for files and readers.

os.ReadFile, WriteFile, ReadDir, CreateTemp, MkdirTemp; io.ReadAll, NopCloser, Discard.

deprecated 1.16

Both

various

List directories as []os.FileInfo.

os.ReadDir returns []fs.DirEntry; io/fs, os.DirFS and //go:embed exist (1.16); os.CopyFS (1.23); os.Root (1.24).

1.16, 1.23, 1.24

Both

various

The global math/rand generator is deterministic until seeded with rand.Seed.

It is randomly seeded (1.20); rand.Seed is a no-op (1.24); math/rand/v2 is the recommended package (1.22).

1.20, 1.22, 1.24

Bodner

11

ServeMux cannot route by method or path variable, so use a third-party router.

Patterns such as GET /books/{isbn} with Request.PathValue; 307 trailing-slash redirects (1.26).

1.22, 1.26

Vivien

11

Prefix-only routing; http.ListenAndServe with no timeouts; Transport.Dial.

Method and wildcard patterns; use http.Server with timeouts and Shutdown; DialContext.

1.22; 1.7

Bodner

11

Advises against DefaultClient and DefaultServeMux.

Still right: construct a Client with a timeout and your own ServeMux. Newer: CrossOriginProtection (1.25) and httptest.NewTestServer (1.27, for synctest).

1.25, 1.27

Both

11; 10

Log with the log package.

log/slog, slog.NewMultiHandler.

1.21, 1.26

Bodner

11

encoding/json v1 only.

omitzero (1.24); encoding/json/v2 and jsontext GA with v1 reimplemented on v2 (1.27).

1.24, 1.27

Bodner

13

Tag integration tests with // +build integration.

//go:build integration.

1.17

Both

13; 12

Write benchmarks with b.N.

b.Loop(); -benchtime=Nx since 1.12.

1.24

Both

13; 12

Tests, table tests and -race only.

Native fuzzing (1.18), t.Setenv (1.17), t.Context, t.Chdir (1.24), testing/synctest (GA 1.25), t.Attr, t.Output (1.25), t.ArtifactDir (1.26), coverage for binaries (1.20).

1.17 to 1.26

Vivien

10

fmt, bufio and text handling as of Go 1.6.

strings.Builder (1.10), the fmt.Append family (1.19), bufio.Scanner.Buffer for long lines, strings.Lines and SplitSeq (1.24).

1.10, 1.19, 1.24

Both

various

Not covered: post-quantum and FIPS cryptography.

Post-quantum crypto/mlkem (1.24), crypto/hpke (1.26), crypto/mldsa (1.27); the FIPS 140-3 module (1.24); govulncheck.

1.24 to 1.27

Both

various

Not covered: unique, weak, finalizer replacement or structs.HostLayout.

unique (1.23), structs.HostLayout (1.23), weak and runtime.AddCleanup (1.24).

1.23, 1.24

Both

various

Not covered: PGO, the execution tracer and the flight recorder.

PGO (preview 1.20, GA 1.21), the new execution tracer (1.22), the flight recorder (1.25), flame graph as the default pprof view (1.26).

1.20 to 1.26

Both

various

Not covered: WebAssembly and WASI.

GOOS=wasip1 and //go:wasmimport (1.21), //go:wasmexport (1.24).

1.21, 1.24

Both

various

Not covered: net/netip, uuid and SIMD.

net/netip (1.18), uuid (1.27), experimental simd and simd/archsimd (GOEXPERIMENT=simd, 1.26 to 1.27).

1.18, 1.27

Errata and bad practices in the books

The statements below are mistakes of the books, not changes of Go. They are listed so that a reader who finds one in an older tutorial knows the correct form; none of the faulty forms is reproduced in this reference.

Book Chapter Mistake (paraphrased) Correct form

Bodner

1

A command for installing a secondary toolchain contains a stray dot in the module path.

The wrapper module is golang.org/dl/go1.N.M, with no dot after go. Today GOTOOLCHAIN makes the wrapper unnecessary.

Bodner

1

The Windows instructions append \bin to PATH and use setx, which truncates long values.

The directory to add is the one that contains go.exe (\go\bin under the installation directory), edited in the system settings dialog; the MSI installer does it for you.

Bodner

3

go vet "blocks" an integer-to-string conversion.

Vet reports it; the compiler accepts it.

Bodner

13

A repeated word in the testing chapter.

A typographical slip only.

Vivien

1

Stringer is built into the language.

It is fmt.Stringer.

Vivien

2

All types are comparable.

Slices, maps and functions are comparable only to nil.

Vivien

2

Untyped constants keep unlimited precision in memory.

Only at compile time.

Vivien

3

A condition that is always true because of a || of overlapping tests.

Vet’s bools analyzer flags redundant and suspect boolean expressions.

Vivien

4

int and uint are 32 bits; a rune stores UTF-8.

64 bits on 64-bit platforms; a rune is a code point.

Vivien

5

A function type is determined by parameters alone.

Results and variadic-ness count too.

Vivien

5

A constructor call on the wrong package for creating an error.

errors.New and fmt.Errorf.

Vivien

5, 9, 10

Ignoring the Close error of a file opened for writing; panic for an ordinary "file exists" condition; goroutine demos whose main returns without waiting.

Check Close (a write may fail only there) or use errors.Join with defer; return errors; wait with a WaitGroup or channel.

Vivien

6

The ; as the GOPATH separator on Linux.

Colon on Unix, semicolon only on Windows; moot with modules.

Vivien

7

copy returns a deep copy with a new array; a string is an array of runes.

A shallow copy into an existing destination; a string is a byte sequence.

Vivien

9

time.After closes its channel.

It sends once.

Vivien

10

Wrong type names for file and writer in the I/O chapter, and a misspelled tool name ("govet" for go vet).

*os.File, io.Writer, go vet.

Vivien

11

A handler that never closes the file it serves, calls WriteHeader after writing the body, sets no Content-Type, accepts any method and body size, and runs ListenAndServe with no timeouts.

http.ServeFileFS/FileServerFS with embed, a method check or a method-aware pattern, http.MaxBytesReader, an http.Server with ReadHeaderTimeout and the other timeouts, and Server.Shutdown.

Vivien

various

Wrong chapter cross-references.

Not applicable.

Source: Effective Go

How to keep up

  • Read the release notes of each release before you upgrade: go.dev/doc/go1.N is short and has a section per area.

  • Raise the go line one release at a time; the stdversion vet check and go test tell you if a file uses a library symbol your go line does not allow.

  • Run go fix -diff ./... on every upgrade. New modernizers arrive each release (four in 1.27).

  • Keep godebug entries in go.mod with a comment saying why and when to remove them.

  • Check the supported releases (1.27 and 1.26 on 2026-10-18) and plan to leave a release before it falls out of support. Go 1.28 is expected in February 2027, and when it ships Go 1.26 stops receiving fixes.

  • For topic-by-topic detail follow the links in the tables to the page of the section that covers it. The cheat sheet collects the current idioms on one page.