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 ( |
The compiler, linker, |
|
The |
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. |
|
|
Run-time behavior changes that could break programs. The |
|
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.
Source: Go 1.27 release notes: go test
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, |
Native fuzzing, |
1.19 |
2022-08-02 |
Memory model revised (June 6, 2022), |
|
1.20 |
2023-02-01 |
Slice to array conversion, |
|
1.21 |
2023-08-08 |
|
|
1.22 |
2024-02-06 |
Per-iteration loop variables, range over integers |
|
1.23 |
2024-08-13 |
Range over functions ( |
Timer rework, |
1.24 |
2025-02-11 |
Generic type aliases |
|
1.25 |
2025-08-12 |
No language change; core types removed from the spec |
Container-aware |
1.26 |
2026-02-10 |
|
Green Tea GC default, |
1.27 |
2026-08-19 |
Generic methods, promoted-field keys in struct literals, wider function type inference |
|
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.
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 |
Tools |
Native fuzzing ( |
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 |
|
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 |
|
Runtime |
The soft memory limit |
Library |
Typed atomics ( |
Go 1.20 (2023-02-01)
| Area | Change |
|---|---|
Language |
Conversion from a slice to an array (and to an array pointer since 1.17); |
Tools |
Profile-guided optimization preview; coverage profiles for whole programs ( |
Runtime |
|
Library |
|
Go 1.21 (2023-08-08)
| Area | Change |
|---|---|
Language |
Builtins |
Tools |
The |
Library |
New packages |
Go 1.22 (2024-02-06)
| Area | Change |
|---|---|
Language |
Each iteration of a |
Tools |
|
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 |
|
Go 1.23 (2024-08-13)
| Area | Change |
|---|---|
Language |
Range over function types ( |
Tools |
The |
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 |
Library |
|
Go 1.24 (2025-02-11)
| Area | Change |
|---|---|
Language |
Generic type aliases ( |
Tools |
|
Runtime |
Swiss-table maps, a new internal mutex and cheaper small-object allocation (2 to 3 percent less CPU on average); |
Library |
|
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 |
|
Runtime |
|
Library |
|
Go 1.26 (2026-02-10)
| Area | Change |
|---|---|
Language |
|
Tools |
|
Runtime |
Green Tea GC enabled by default (10 to 40 percent less GC overhead in GC-heavy programs; |
Library |
|
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 |
|
Runtime |
Size-specialized allocation (up to 30 percent cheaper for small objects, about 60 KB of binary growth, |
Library |
|
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.
Source: Go 1.18 release notes
Source: Go 1.19 release notes
Source: Go 1.20 release notes
Source: Go 1.21 release notes
Source: Go 1.22 release notes
Source: Go 1.23 release notes
Source: Go 1.24 release notes
Source: Go 1.25 release notes
Source: Go 1.26 release notes
Source: Go 1.27 release notes
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", ¬Found{"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
Source: sync: WaitGroup.Go
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 |
A new toolchain with an old |
3 |
|
Fix what vet reports first. The checks are cheap and |
4 |
Raise the |
This turns on the language and |
5 |
|
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, |
These need judgement; the sections below show the current form of each. |
7 |
Remove |
Each entry is a deferral, not a fix. |
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))
}
Source: cmd/go: Build constraints, io/ioutil (deprecated) and math/rand
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: Go 1.26 release notes: go fix
Source: cmd/fix
Source: unsafe: StringData
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).
Source: Using Go Modules
Source: Go Modules Reference
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.
Source: Managing dependencies: Tools
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 |
|---|---|---|
|
|
About twice as fast and allocating about half as much since Go 1.26 |
|
|
Same behavior |
|
|
Same behavior |
|
|
Returns |
|
|
The pattern may contain a |
|
|
Same pattern rule |
|
|
Same behavior |
|
|
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.
Source: io/ioutil (deprecated)
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) |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.
Source: cmd/go: Build constraints
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 |
|---|---|
|
|
|
|
|
|
|
|
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.
Source: Fixing For Loops in Go 1.22
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.
Source: Routing Enhancements for Go 1.22
Source: net/http: ServeMux
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.
Source: 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.
Source: Migrating to encoding/json/v2
Source: encoding/json/v2
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 |
|---|---|
|
Formatting and the correctness analyzers; |
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. |
|
Applies the modernizers (Go 1.26), including the source-level inliner for your own |
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. |
|
Pins the version of a community tool in |
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: Go 1.26 release notes: go fix
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 |
|---|---|---|
|
|
1.21 |
A loop that searches a slice for a value |
|
1.21 |
|
|
1.26 |
|
|
1.24 |
|
|
1.20 |
|
|
1.18, 1.27 |
|
|
1.25 |
|
|
1.21 |
A loop that deletes all keys of a map |
|
1.21 |
Helper |
|
1.26 |
|
|
1.22 |
|
|
1.22 |
|
|
1.19 |
|
|
1.7 |
|
|
1.24 |
Stopping and draining a timer channel before |
Plain |
1.23, setting removed 1.27 |
Relying on |
Check for |
1.21 |
|
|
1.24 |
|
Iterator methods and |
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.
Source: runtime: PanicNilError
Source: Go 1.18 release notes: Runtime
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 |
|---|---|
|
|
|
|
|
checks |
|
simplifies references to embedded fields in composite literals |
|
|
|
removes the redundant re-declaration of loop variables |
|
checks addresses passed to |
|
applies |
|
loops over maps to calls to the |
|
|
|
helper functions that take the address of a value to |
|
suggests |
|
removes obsolete old-style build comments |
|
three-clause counting loops to |
|
|
|
backward loops over slices to |
|
search loops to |
|
|
|
|
|
|
|
|
|
|
|
ranging over |
|
|
|
unsafe pointer arithmetic to function calls |
|
|
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.
Source: Using the source-level inliner
Source: Go fix: the Go Blog
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
goline of the main module picks the defaults of that version. Ago 1.20module on a 1.27 toolchain behaves like Go 1.20 for every setting that exists. -
A project overrides single settings in three places: a
godebugdirective ingo.mod(Go 1.23), a//go:debugcomment in apackage mainsource file, or theGODEBUGenvironment variable at run time (which wins). -
go version -m binaryprints theDefaultGODEBUGthe 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
gocommand 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 |
|---|---|---|---|
|
1.21 |
|
|
|
1.22 |
|
The pre-1.22 |
|
1.22 |
|
Lenient handling of an invalid |
|
1.23 |
|
Keeps some headers on error responses from |
|
1.23 |
|
Older Windows symbolic-link and |
|
1.23 |
|
Allows certificates with negative serial numbers |
|
1.24 |
|
|
|
1.24 |
|
Allows RSA keys under 1024 bits |
|
1.24 |
|
Multipath TCP is not used by default for listeners |
|
1.24 |
|
No limit on the number of cookies or query parameters (the limits were backported as security fixes) |
|
1.24 |
|
Text rather than JSON build output in |
|
1.24 |
|
Older certificate and policy handling |
|
1.25 |
|
|
|
1.25 |
|
Allows SHA-1 signature algorithms in TLS 1.2 |
|
1.25 |
|
SHA-1 based subject key identifiers |
|
1.25 |
|
No |
|
1.26 |
|
Accepts hostnames with colons outside brackets |
|
1.26 |
|
No limit on the number of ranges in |
|
1.26 |
|
|
|
1.24, 1.26 |
|
No hybrid post-quantum key exchange in the default TLS configuration |
|
1.27 |
|
No goroutine labels in tracebacks |
|
1.27 |
|
|
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 |
|---|---|---|
|
1.24 |
Certificates signed with SHA-1 are rejected; the Kyber draft key exchange is gone (the |
|
1.27 |
Timer and ticker channels are always synchronous and unbuffered, in every module |
|
1.27 |
|
|
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.
Source: Go 1.27 release notes: Runtime
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 |
Modules: a |
1.11, default 1.16 |
Vivien, Bodner |
1, 6; 1 |
|
|
1.17, 1.18 |
Bodner |
1 |
Install further Go versions with |
The |
1.21 |
Both |
1 |
Fetch dependencies straight from version control hosts. |
Module proxy and checksum database by default ( |
1.13 |
Both |
various |
Keep tool dependencies in a blank-import |
|
1.24 |
Bodner |
9 |
Describes |
Adds |
1.18, 1.25, 1.27 |
Bodner |
1 |
Install |
gopls organizes imports; |
current |
Vivien |
1 |
The |
|
1.25, 1.27 |
Both |
1 |
Format and vet are separate habits; doc comments are plain text. |
|
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. |
|
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 |
|
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 |
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 |
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 |
|
It is |
1.18 ( |
Vivien |
4 |
Complete literal list omits |
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 |
|
Vet reports it as a diagnostic; it is not a compile error. Use |
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. |
|
1.17, 1.20, 1.21 |
Vivien |
7 |
|
|
always |
Bodner |
6 |
Take a pointer to a literal with a helper function or a temporary variable. |
|
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, |
1.18 |
Bodner |
15 |
Generics through the draft design with "type lists" and a special Go2Go playground. |
Final syntax with unions, |
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 |
1.22, 1.23, 1.24 |
Both |
4, 10; 3, 9 |
Copy the loop variable ( |
A new copy per iteration; the copies are redundant. |
1.22 |
Both |
2 |
Quote the unused-variable error as |
Go 1.27 prints |
reworded |
Errors, concurrency and the runtime
| Book | Chapter | What the book says (paraphrased) | Current behavior | Version |
|---|---|---|---|---|
Vivien |
1, 5 |
Compare sentinel errors with |
Wrap with |
1.13, 1.20, 1.26 |
Both (era) |
various |
|
|
1.21 |
Bodner |
8 |
Convert a panic to an error at a library boundary with |
Still sound, with two additions: |
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 |
|
It also follows cgroup CPU limits on Linux and is updated at run time. |
1.25 |
Vivien |
9 |
Cancels goroutines with done channels, because |
|
1.7, 1.20, 1.21 |
Bodner |
12 |
Teaches |
Still right; the cause variants, |
1.20, 1.21 |
Bodner |
10 |
Use |
Typed atomics (1.19), |
1.19, 1.21 |
Both |
10; 9 |
Launch goroutines with |
|
1.25 |
Both |
10; 9 |
Drain a timer’s channel before |
Unreferenced timers are collectable and channels are synchronous, so |
1.23 (setting removed 1.27) |
Vivien |
9 |
|
It sends a single value and never closes. |
always |
Both |
10; 9 |
Find concurrency bugs with |
The revised Memory Model (1.19, sequentially consistent atomics), |
1.19, 1.25, 1.27 |
Both |
6; 10 |
Tune the collector with |
|
1.19, 1.24, 1.26, 1.27 |
Bodner |
14 |
Convert between strings and byte slices with the header structs of |
Those header types are deprecated; use |
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 |
1.24, 1.26 |
Standard library
| Book | Chapter | What the book says (paraphrased) | Current behavior | Version |
|---|---|---|---|---|
Both |
various |
Use |
|
deprecated 1.16 |
Both |
various |
List directories as |
|
1.16, 1.23, 1.24 |
Both |
various |
The global |
It is randomly seeded (1.20); |
1.20, 1.22, 1.24 |
Bodner |
11 |
|
Patterns such as |
1.22, 1.26 |
Vivien |
11 |
Prefix-only routing; |
Method and wildcard patterns; use |
1.22; 1.7 |
Bodner |
11 |
Advises against |
Still right: construct a |
1.25, 1.27 |
Both |
11; 10 |
Log with the |
|
1.21, 1.26 |
Bodner |
11 |
|
|
1.24, 1.27 |
Bodner |
13 |
Tag integration tests with |
|
1.17 |
Both |
13; 12 |
Write benchmarks with |
|
1.24 |
Both |
13; 12 |
Tests, table tests and |
Native fuzzing (1.18), |
1.17 to 1.26 |
Vivien |
10 |
|
|
1.10, 1.19, 1.24 |
Both |
various |
Not covered: post-quantum and FIPS cryptography. |
Post-quantum |
1.24 to 1.27 |
Both |
various |
Not covered: |
|
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. |
|
1.21, 1.24 |
Both |
various |
Not covered: |
|
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 |
Bodner |
1 |
The Windows instructions append |
The directory to add is the one that contains |
Bodner |
3 |
|
Vet reports it; the compiler accepts it. |
Bodner |
13 |
A repeated word in the testing chapter. |
A typographical slip only. |
Vivien |
1 |
|
It is |
Vivien |
2 |
All types are comparable. |
Slices, maps and functions are comparable only to |
Vivien |
2 |
Untyped constants keep unlimited precision in memory. |
Only at compile time. |
Vivien |
3 |
A condition that is always true because of a |
Vet’s |
Vivien |
4 |
|
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. |
|
Vivien |
5, 9, 10 |
Ignoring the |
Check |
Vivien |
6 |
The |
Colon on Unix, semicolon only on Windows; moot with modules. |
Vivien |
7 |
|
A shallow copy into an existing destination; a string is a byte sequence. |
Vivien |
9 |
|
It sends once. |
Vivien |
10 |
Wrong type names for file and writer in the I/O chapter, and a misspelled tool name ("govet" for |
|
Vivien |
11 |
A handler that never closes the file it serves, calls |
|
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.Nis short and has a section per area. -
Raise the
goline one release at a time; thestdversionvet check andgo testtell you if a file uses a library symbol yourgoline does not allow. -
Run
go fix -diff ./...on every upgrade. New modernizers arrive each release (four in 1.27). -
Keep
godebugentries ingo.modwith 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.