Toolchain Versions and Compatibility

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) and was checked on 2026-10-18. Go 1.28 is not released yet: the expected date is February 2027, no 1.28 release candidate exists, and the go.dev/doc/go1.28 page is an empty draft, so nothing here depends on it. Three questions run through the page: which Go versions exist and for how long they are supported, what stays compatible when a new release arrives, and how a project says which toolchain and which behaviors it needs. The answers are the release policy, the compatibility promise, and the go and toolchain lines together with GOTOOLCHAIN, GODEBUG and GOEXPERIMENT. The previous page is Getting Started; version-dependent language rules come up again on What’s New and Migrating Older Code and the module files on Modules and Dependency Management.

Every output below was produced with go1.27.2 on darwin/arm64. Commands that would download a toolchain were run with GOPROXY=off, so the failures they show are the real offline results.

The release cycle and the support policy

Go is released every six months, in February and August. The Go team describes the cycle as about four months of development followed by three months of release freeze, with the development of the next release starting before the previous one ships. The target milestones, each aligned to a week of a month:

Milestone What happens

January and July, week 3

the tree opens for general development of the next release

May and November, week 4

the release freeze starts: only bug fixes, test-only changes and documentation are accepted

June and December, week 2

release candidate 1 (1.NrcR) is issued; later candidates only for critical bugs

August and February, week 2

the release is issued, as 1.N.0

After the release

minor releases (1.N.1, 1.N.2, …​) with fixes for critical bugs and security issues

Exact dates move with holidays, but the cycle is steady, as the history shows:

Release Date Status on 2026-10-18

Go 1.18

2022-03-15

out of support

Go 1.19

2022-08-02

out of support

Go 1.20

2023-02-01

out of support

Go 1.21

2023-08-08

out of support

Go 1.22

2024-02-06

out of support

Go 1.23

2024-08-13

out of support

Go 1.24

2025-02-11

out of support

Go 1.25

2025-08-12

out of support since Go 1.27 shipped

Go 1.26

2026-02-10

supported (latest patch go1.26.9)

Go 1.27

2026-08-19

supported (latest patch go1.27.2, released 2026-10-08)

Go 1.28

expected 2027-02

not released

Dates are the dates of the .0 releases. The support policy is one sentence: each major Go release is supported until there are two newer major releases. So with 1.27 current, both 1.27 and 1.26 receive fixes for critical problems, including security problems, as minor releases; when Go 1.28 ships, Go 1.26 stops. Three consequences follow for projects: a production service should be on the current or previous release, a library should say which older release it still needs (the go line, below), and a CI matrix of "current and previous" matches what the Go team itself supports.

timeline title Go releases 1.18 to 1.28 and what each one changed for day-to-day code 2022-03 : Go 1.18 : type parameters : fuzzing : workspaces 2022-08 : Go 1.19 : revised memory model : typed atomics : GOMEMLIMIT : doc comment syntax 2023-02 : Go 1.20 : errors.Join : slice to array conversion : PGO preview : coverage for binaries 2023-08 : Go 1.21 : min, max and clear : log/slog : slices and maps : toolchain line : PGO GA 2024-02 : Go 1.22 : per-iteration loop variables : range over integers : ServeMux patterns : math/rand/v2 2024-08 : Go 1.23 : range over functions and iter : unique : timer channels collectable : godebug directive 2025-02 : Go 1.24 : generic type aliases : os.Root : weak and AddCleanup : tool directives : b.Loop 2025-08 : Go 1.25 : container-aware GOMAXPROCS : WaitGroup.Go : synctest GA : flight recorder 2026-02 : Go 1.26 : new with an expression : self-referential constraints : Green Tea GC default : go fix modernizers 2026-08 : Go 1.27 : generic methods : encoding/json/v2 GA : goroutine leak profile GA : uuid 2027-02 : Go 1.28 : expected - not released

The diagram lists the headline changes each release brought; each topic has its own page in this section. The toolchain line and the godebug directive on the timeline are the subject of this page.

Version names

Go versions have a strict syntax, and the toolchain compares them precisely. This matters whenever a go.mod file names a version.

Name Meaning

1.N.P

release P of Go 1.N: 1.27.0 is the first release of Go 1.27, 1.27.2 a patch release

1.NrcR

release candidate R of Go 1.N, issued before 1.N.0, for example 1.27rc1

1.N

the language version: the whole family of releases implementing that version of the language and standard library; 1.27, 1.27rc1 and 1.27.2 all have language version 1.27

go1.27.2

a toolchain name: go followed by a version denoting a beta, a release candidate or a release; these names are what go version, runtime.Version and the files named in PATH use

go1.27

a toolchain name only for old releases (go1.20, go1.19); from Go 1.21 on, the first release is go1.21.0, so go1.27 names a toolchain built from the development tree

go1.27.2-custom

a non-standard toolchain: any suffix after a dash is allowed and ignored when versions are compared

The ordering inside a language version, from least to greatest, is the language version itself, then release candidates by their number, then releases by patch number: 1.21 < 1.21rc1 < 1.21rc2 < 1.21.0 < 1.21.1. A different language version decides the comparison first, so 1.21.9 < 1.22. Before Go 1.21 the first release of a toolchain was just 1.N and sat after its release candidates (1.20rc3 < 1.20 < 1.20.1), and beta releases such as 1.18beta2 sorted before release candidates.

$ go version
go version go1.27.2 darwin/arm64

The Go 1 compatibility promise

The compatibility promise is why a Go program from 2015 usually builds with today’s toolchain. Go 1 defines the language specification and the "standard packages" of the core library, and the promise is that programs written to that specification continue to compile and run correctly, unchanged, over the lifetime of the specification. Compatibility is at the source level: binaries are not guaranteed to link across releases, so you recompile after an upgrade. New releases may add APIs and features, but not in a way that breaks existing Go 1 code. Each set of release notes repeats the promise in its first paragraph, and the Go 1.27 notes expect almost all programs to continue to compile and run as before.

The promise has documented exceptions. They are narrow, but a robust program should know them:

Exception What it means

Security

a security problem in the specification or implementation may require an incompatible fix

Unspecified behavior

code that depends on something the specification leaves open, such as map iteration order or the layout of a type, may break

Specification errors

an inconsistency or gap in the spec may be fixed, which can change the meaning or legality of a program

Bugs

code that depends on a compiler or library bug may break when the bug is fixed

Unkeyed struct literals

fields can be added to an exported struct in a later release, so pkg.T{3, "x"} may stop compiling while the keyed form pkg.T{A: 3, B: "x"} keeps working

Added methods

adding a method to a non-interface type can create a conflict when that type is embedded next to another with the same method name

Dot imports

import . "path" may start colliding with new exported names

Package unsafe

programs that use it may depend on implementation details that change

The syscall package

outside the promise, because operating-system interfaces change; frozen since Go 1.4, with evolution in golang.org/x/sys

Sub-repositories

code under golang.org/x/... follows looser rules, though tags identify versions compatible with each Go 1 point release

The toolchain itself

scripts that depend on the location or behavior of compilers, linkers and go subcommands may break

Performance

no guarantee that a program does not get slower in a new release

Features still in development

unreleased and experimental features (GOEXPERIMENT) carry no promise

The unkeyed-literal rule is the one most likely to affect ordinary code, and go vet knows it. This program builds a csv.ParseError both ways; both work today, but only one is guaranteed to keep working:

package main

import (
	"encoding/csv"
	"fmt"
)

func main() {
	unkeyed := csv.ParseError{1, 2, 3, csv.ErrFieldCount}
	keyed := csv.ParseError{StartLine: 1, Line: 2, Column: 3, Err: csv.ErrFieldCount}
	fmt.Println(unkeyed.Error() == keyed.Error())
	fmt.Println(keyed.Error())
}
$ go run .
true
record on line 2: wrong number of fields

$ go vet
main.go:9:13: encoding/csv.ParseError struct literal uses unkeyed fields

Compatibility has a second mechanism for the cases the promise lets through. When a change is allowed by Go 1 but may break some programs (a security fix, say, or HTTP/2 enabled by default), the Go team adds a GODEBUG setting that opts a program back into the old behavior, and, since Go 1.21, makes that setting follow the module’s go line automatically. That mechanism has its own section below. The blog post Backward Compatibility, Go 1.21, and Go 2 explains the thinking: the go line has stopped being advisory and has become a contract between the code and the toolchain.

The go line and the toolchain line

The go.mod file (and go.work for workspaces) states two versions:

  • go 1.27 (the go line) is the minimum Go version needed to use the module. Before Go 1.21 it was advisory: toolchains built the code and only printed a note when the build failed. Go 1.21 made it mandatory. A toolchain refuses to load a module whose go line is newer than itself, and a module’s go line must be at least as new as the go line of every module it requires. Releases starting at Go 1.19.13 and Go 1.20.8 refuse modules that declare 1.22 or later.

  • toolchain go1.27.2 (the toolchain line) is a suggested toolchain. When the go line sets the minimum, the toolchain line says what to prefer when you work in this module directly. A library may say go 1.21 so that others can depend on it with an old Go, and toolchain go1.27.2 so that its own developers use a current one.

If the go line is missing, the module is treated as having go 1.16 (a missing go line in go.work means go 1.18). If the toolchain line is missing, it is implied: go 1.21.0 with no toolchain line is read as toolchain go1.21.0. The version forms go 1.27 and go 1.27.0 differ since Go 1.21: 1.27 denotes a language version that is older than every release of it, so use it when you do not need a specific patch release.

module example.com/shelf

go 1.27

toolchain go1.27.2

The go line has a second job. It sets the language version the compiler enforces for every package in the module, and the toolchain turns it into -lang=go1.27 on the compile command, as the previous page showed. A newer toolchain compiling an older module therefore still follows the older language rules where they differ. The demonstration has three parts.

Part 1: a language feature that is too new for the go line. Range over an integer arrived in Go 1.22. A module that says go 1.21 cannot use it, even when the toolchain is Go 1.27:

package main

import "fmt"

func main() {
	for i := range 3 {
		fmt.Println(i)
	}
}
$ cat go.mod
module example.com/langerr

go 1.21

$ go build
# example.com/langerr
./main.go:6:17: cannot range over 3 (untyped int constant): requires go1.22 or later (-lang was set to go1.21; check go.mod)

$ go vet
# example.com/langerr
# [example.com/langerr]
vet: ./main.go:6:17: cannot range over 3 (untyped int constant): requires go1.22 or later

Part 2: the same source behaves differently under different go lines. Go 1.22 changed for loops so that each iteration has its own copy of the loop variable. The change is gated on the go line, so existing code keeps its old meaning until you raise the line:

package main

import "fmt"

func main() {
	var printers []func()
	for i := 0; i < 3; i++ {
		printers = append(printers, func() { fmt.Print(i, " ") })
	}
	for _, p := range printers {
		p()
	}
	fmt.Println()
}
$ go run .          # go.mod says go 1.21
3 3 3
$ go run .          # go.mod says go 1.22
0 1 2
$ go run .          # go.mod says go 1.27
0 1 2

Raising the go line is therefore a behavior change as well as a permission, which is why a go line bump belongs in a reviewed commit (and why the go fix modernizers described in Formatting, vet, fix and Static Analysis help afterwards).

Part 3: a file can raise its own language version. A //go:build constraint that implies at least go1.21 sets the language version of that one file to the constraint’s version. A module that supports go 1.21 can therefore carry one file with newer syntax and a fallback for older toolchains:

module example.com/langver

go 1.21
//go:build go1.22

package main

func count() int {
	n := 0
	for range 3 {
		n++
	}
	return n
}
//go:build !go1.22

package main

func count() int {
	n := 0
	for i := 0; i < 3; i++ {
		n++
	}
	return n
}
package main

import "fmt"

func main() {
	fmt.Println("copies:", count())
}
$ go vet . && go run .
copies: 3
$ go list -f '{{.GoFiles}} {{.IgnoredGoFiles}}' .
[count_new.go main.go] [count_old.go]

Standard-library symbols that are too new

The language version covers syntax and semantics, but it does not stop you from calling a standard-library function that only exists in a newer release. The compiler is happy, because it has the whole standard library of the running toolchain, but the program would fail to build on the oldest toolchain the go line promises to support. The stdversion vet analyzer catches it, and since Go 1.27 go test runs it automatically:

package main

import (
	"fmt"
	"maps"
	"slices"
)

func main() {
	titles := map[string]int{"Emma": 1, "Dune": 2}
	fmt.Println(slices.Sorted(maps.Keys(titles)))
}
$ cat go.mod
module example.com/stdv

go 1.22

$ go build && echo build-ok
build-ok
$ go vet
main.go:11:21: slices.Sorted requires go1.23 or later (module is go1.22)
main.go:11:33: maps.Keys requires go1.23 or later (module is go1.22)
$ go test
# example.com/stdv
# [example.com/stdv]
./main.go:11:21: slices.Sorted requires go1.23 or later (module is go1.22)
./main.go:11:33: maps.Keys requires go1.23 or later (module is go1.22)
FAIL	example.com/stdv [build failed]

(The go test run used a package that had one test file. Files with a //go:build go1.23 constraint are exempt, as in Part 3 above.)

Changing the lines: go get, go mod edit, go work edit

The go and toolchain lines are versioned dependencies of the module on the Go toolchain, and go get manages them as it manages require lines:

Command Effect on go.mod

go get go@1.27

sets go 1.27 to the newest available release of language version 1.27 and brings dependencies along as needed; the simple replacement for go mod tidy -go=1.27

go get go@latest

requires the latest released toolchain

go get go@1.22.1 toolchain@1.24rc1

go 1.22.1 and toolchain go1.24rc1

go get toolchain@none

removes the toolchain line

go get go@1.27.2 toolchain@none

sets the go line and removes the toolchain line (it is redundant when it equals the go line)

go get toolchain@go1.22.9

changes only the toolchain line, unless it would fall below the go line, in which case it lowers both

go get example.com/widget@v1.2.3

if widget needs a newer Go, raises the go line and records the toolchain that ran the command

go get needs the network to query the available versions, so those commands are listed here from the documentation rather than run in the offline sandbox. The offline-safe editor for the files is go mod edit (and go work edit for go.work):

$ go mod edit -toolchain=go1.27.2
$ cat go.mod
module example.com/tc

go 1.27

toolchain go1.27.2
$ go mod edit -go=1.27.2 -toolchain=none
$ cat go.mod
module example.com/tc

go 1.27.2
$ go mod edit -go=1.27 -toolchain=default
$ go mod edit -json
{
	"Module": {
		"Path": "example.com/tc"
	},
	"Go": "1.27",
	"Toolchain": "default"
}

The value toolchain default means "the default toolchain is good enough": it stops the go command from switching to a newer toolchain on account of this file, as explained next. go get and go mod tidy keep the go line at least as new as the go line of every dependency; go work use (with no arguments) repairs a go.work whose go line has drifted below one of its modules; and go work edit -toolchain=none removes the toolchain line from a workspace file.

GOTOOLCHAIN and automatic toolchain selection

The go command that you install is a launcher as well as a toolchain. At startup it decides which toolchain to run, using the GOTOOLCHAIN setting and the lines in the current module or workspace. The setting itself is found like any Go environment setting: a non-empty value in the process environment wins, then the user’s default file (go env -w, see below), then the $GOROOT/go.env file inside the installation, which in a standard toolchain says GOTOOLCHAIN=auto; with no default at all it behaves as local.

Value Meaning

local

always run the bundled toolchain, the one that shipped with this go command; if the module needs something newer, stop with an error

go1.27.9 (a toolchain name)

always run that exact toolchain: found in PATH under that name, or else downloaded and verified

auto (shorthand for local+auto)

the default: start with the bundled toolchain and switch to a newer one when the module’s toolchain or go line demands it, looking in PATH first and downloading otherwise

path (shorthand for local+path)

as auto, but only search PATH; never download

<name>+auto and <name>+path

the same, but starting from <name> as the default instead of the bundled toolchain

The selection rules, after reading go.work (or go.mod when there is no workspace) in the current directory, are exactly these:

flowchart TD A["go command starts"] --> B["Find GOTOOLCHAIN: process environment,
then go env -w file, then GOROOT/go.env"] B --> C{"Which form?"} C -->|"local"| L["Run the bundled toolchain.
Error if go line is newer than it"] C -->|"a toolchain name such as go1.27.9"| X["Run exactly that toolchain:
PATH first, else download"] C -->|"auto, path, name+auto or name+path"| D["Default toolchain is name, or local.
Read toolchain and go lines"] D --> E{"toolchain line?"} E -->|"toolchain default"| F["Run the default toolchain.
Never switch on account of this file"] E -->|"newer than the default"| G["Switch to that toolchain"] E -->|"absent, or not newer"| H{"go line newer
than the default?"} H -->|"no"| J["Run the default toolchain"] H -->|"yes"| I["Switch to the toolchain named by the go line"] G --> K{"Where may it come from?"} I --> K K -->|"auto"| M["Look for the name in PATH,
else download golang.org/toolchain module"] K -->|"path"| N["Look in PATH only.
Error if it is not there"]

Here are the real behaviors, with a module whose go line is deliberately newer than anything installed. First the failing cases, offline:

$ cat go.mod
module example.com/tc

go 1.99

$ GOTOOLCHAIN=local go build
go: go.mod requires go >= 1.99 (running go 1.27.2; GOTOOLCHAIN=local)

$ GOTOOLCHAIN=auto go build
go: downloading go1.99.0 (darwin/arm64)
go: download go1.99.0 for darwin/arm64: toolchain not available

$ GOTOOLCHAIN=path go build
go: cannot find "go1.99.0" in PATH

local refuses to continue; auto tries to download go1.99.0 and, with no network and no such toolchain, fails; path only searches PATH. The same module with a toolchain line that is newer than the default would be switched to that toolchain the same way; a toolchain line older than the running toolchain is ignored, so with GOTOOLCHAIN=local a line such as toolchain go1.99.1 does not stop a build (the go line is what local enforces).

To watch the success path without network, put a stand-in program named go1.27.9 on PATH. This one is a two-line shell script that only prints its arguments, so it also proves the mechanism: the go command runs the toolchain with the matching name and forwards the arguments. The module says go 1.27.9, newer than the bundled go1.27.2:

$ cat fakebin/go1.27.9
#!/bin/sh
echo "this is a stand-in for go1.27.9; args: $*"

$ PATH=$PWD/fakebin:$PATH GODEBUG=toolchaintrace=1 GOTOOLCHAIN=auto go version
go: upgrading toolchain to go1.27.9 (required by go line in go.mod; upgrade allowed by GOTOOLCHAIN=auto)
go: using go1.27.9 toolchain located in system PATH (…/fakebin/go1.27.9)
this is a stand-in for go1.27.9; args: version

$ PATH=$PWD/fakebin:$PATH GOTOOLCHAIN=path go build
this is a stand-in for go1.27.9; args: build

$ PATH=$PWD/fakebin:$PATH GOTOOLCHAIN=local go build
go: go.mod requires go >= 1.27.9 (running go 1.27.2; GOTOOLCHAIN=local)

$ PATH=$PWD/fakebin:$PATH GOTOOLCHAIN=go1.27.9 go env GOVERSION
this is a stand-in for go1.27.9; args: env GOVERSION

(The path to fakebin is shortened.) GODEBUG=toolchaintrace=1, available since Go 1.24, prints why a toolchain was chosen and where it was found; it is the first thing to try when the wrong toolchain runs. When the requested toolchain is the running one, the trace says go: using local toolchain go1.27.2.

Choosing the toolchain to download

When the go command must fetch a toolchain, it asks for the list of available ones and finds up to three candidates: the latest release candidate of an unreleased language version, the latest patch release of the most recent release, and the latest patch release of the previous release. These are exactly the supported releases. Consistent with minimal version selection, it then picks the oldest candidate that satisfies the requirement. With an illustrative list of go1.28rc1, go1.27.2 and go1.26.9 available, a module asking for go 1.26.4 would get go1.26.9, one asking for 1.27 would get go1.27.2, and one asking for 1.28 would get go1.28rc1. If the requirement is older than every supported release, the oldest supported one is used.

Commands that bring in new module versions, go get, go work use, go work sync, go install pkg@version and go run pkg@version, may meet a module whose go line is newer than the running toolchain. With auto or path the command switches and prints a notice of this form:

go: module example.com/widget@v1.2.3 requires go >= 1.24rc1; switching to go 1.27.9

When the command writes a new go line, it writes the toolchain line of the toolchain that ran it, so the next command in that module starts directly on the right toolchain, and the notice does not appear again. The exceptions are go install pkg@version and go run pkg@version, which run outside any module and so cannot record anything: they print the notice every time.

How toolchains are downloaded

A toolchain is distributed as a module with the path golang.org/toolchain and a version such as v0.0.1-go1.27.9.darwin-arm64. It is downloaded like any module: through GOPROXY, checked against the checksum database (GOSUMDB), and stored in the module cache. A few details follow from that:

  • the version depends on the host operating system and architecture, so checksums cannot be written to go.sum; with GOSUMDB=off toolchain downloads fail for lack of verification, and GOPRIVATE and GONOSUMDB patterns do not apply to them;

  • a downloaded toolchain is cached, so each version is fetched once per machine;

  • go env -w GOTOOLCHAIN=go1.27.2+auto is equivalent to replacing your Go 1.21 or newer installation with Go 1.27.2 as the default: the installed go command is just the launcher;

  • to test with a release candidate for a single command, GOTOOLCHAIN=go1.28rc1 go test ./... runs it without editing any file, once such a release candidate exists.

The sandbox for this page has no network, so the actual download was not run; the command forms and the failure messages above are real.

go env, user defaults and the environment file

GOTOOLCHAIN is one of many settings that live in go env. The previous page showed go env -w, -u, -changed and -json in detail; two behaviors matter here. A value from go env -w is stored in a per-user file (go env GOENV prints its path) and is overridden by the process environment. And because GOTOOLCHAIN can be set that way, one line pins the whole machine to a toolchain default:

$ go env GOTOOLCHAIN
auto
$ go env -w GOTOOLCHAIN=go1.27.2+auto
$ go env -changed | grep GOTOOLCHAIN
GOTOOLCHAIN='go1.27.2+auto'
$ go env -u GOTOOLCHAIN

(Run against a scratch GOENV file, as before.) go env -w is convenient for GOTOOLCHAIN, GOPROXY, GOPRIVATE and GOFLAGS, but not for GOOS and GOARCH: the command accepts them, and then every later build on the machine targets the other platform until you run go env -u. Set those per command.

GODEBUG: keeping old behavior available

A GODEBUG setting is a key=value pair that controls one part of a Go program’s behavior. The environment variable holds a comma-separated list (GODEBUG=http2client=0,http2server=0 disables HTTP/2 in both the client and the server); unknown settings in the variable are ignored. What makes GODEBUG central to compatibility is that a program also has default values compiled in, and the go line selects them.

The rules for the defaults of a setting not named in the environment, in order of precedence:

  1. explicit //go:debug lines in the program’s main package (Go 1.21);

  2. godebug directives in the main module’s go.mod or the workspace’s go.work (Go 1.23);

  3. the defaults of the Go version in the go line (and the special key default=goX.Y in either place);

  4. the defaults of the toolchain that built the program.

So a module that says go 1.20, built with Go 1.27, gets the defaults of Go 1.20 wherever a GODEBUG setting covers a difference. A runnable case is panic(nil): before Go 1.21 it could be recovered as nil; since Go 1.21 it panics with a *runtime.PanicNilError, controlled by the panicnil setting:

package main

import "fmt"

func try() (msg string) {
	defer func() {
		r := recover()
		msg = fmt.Sprintf("recovered %T: %v", r, r)
	}()
	panic(nil)
}

func main() {
	fmt.Println(try())
}
$ cat go.mod            # go 1.27
module example.com/gd

go 1.27
$ go run .
recovered *runtime.PanicNilError: runtime error: panic called with nil argument

$ cat go.mod            # go 1.20
module example.com/gd

go 1.20
$ go run .
recovered <nil>: <nil>
$ go list -f '{{.DefaultGODEBUG}}' . | tr ',' '\n' | grep -E 'panicnil|randseednop|httpmuxgo121'
httpmuxgo121=1
panicnil=1
randseednop=0

go list -f '{{.DefaultGODEBUG}}' <main package> prints the defaults compiled into a main package: only the differences from the base toolchain defaults, which is why the go 1.27 module prints nothing. The same list is recorded in the binary and shown by go version -m as a DefaultGODEBUG entry in its build settings. The full list for a go 1.20 module (abridged above to three settings) shows how many separate behaviors the go line governs.

There are four ways to change a setting. First, at run time, for one process, with the environment variable:

$ GODEBUG=panicnil=1 go run .
recovered <nil>: <nil>

Second, for the whole module, with a godebug directive in go.mod, as a single line:

module example.com/gd

go 1.27

godebug panicnil=1

Third, as a block, using the special key default to take every unlisted setting from a Go version, separately from the module’s language version. This one asks for Go 1.21 semantics and then for the old panic(nil) behavior:

module example.com/gd

go 1.27

godebug (
	default=go1.21
	panicnil=1
)

Fourth, for one program, with a //go:debug directive at the top of a file of its main package:

//go:debug panicnil=1

package main

import "fmt"

func try() (msg string) {
	defer func() {
		r := recover()
		msg = fmt.Sprintf("recovered %T: %v", r, r)
	}()
	panic(nil)
}

func main() {
	fmt.Println(try())
}
$ go run .      # with godebug in go.mod, or with //go:debug in main.go
recovered <nil>: <nil>
$ go list -f '{{.DefaultGODEBUG}}' .
panicnil=1

Some rules to remember:

  • only the main module’s go.mod is consulted for godebug directives; those in dependencies are ignored, and when a workspace is in use go.work replaces the go.mod files for this purpose;

  • //go:debug lines go at the top of a file of a main package, before the package clause; in _test.go files they apply to the test’s main package; everywhere else go vet reports them as misplaced;

  • an unknown setting is an error in godebug directives and in //go:debug lines, and so is listing the same setting twice in //go:debug; toolchains older than Go 1.23 reject godebug lines altogether, and older than 1.21 ignore //go:debug;

  • a setting introduced for a security release applies the new behavior to all go lines.

$ go build            # go.mod contains: godebug nosuchsetting=1
go: error loading go.mod:
go.mod:5: unknown godebug "nosuchsetting"

$ go build            # main.go starts with: //go:debug nosuchsetting=1
# example.com/gd
./main.go:1:1: invalid //go:debug: unknown //go:debug setting "nosuchsetting"

$ go vet              # //go:debug placed after the package clause
main.go:3:1: //go:debug directive only valid before package declaration

The two-year policy and removed settings

Settings added for compatibility are maintained for at least two years, four Go releases. Some, such as http2client and http2server, are kept much longer or indefinitely. When possible each setting has a counter in runtime/metrics, named /godebug/non-default-behavior/<name>:events, that counts how often a program ran with a non-default value, which lets you check whether you still depend on an old behavior before the setting disappears.

Go 1.27 removed seven settings that had reached the end of their lifetime: asynctimerchan, gotypesalias, tls10server, tls3des, tlsrsakex, tlsunsafeekm and x509keypairleaf. It also changed how the go command treats them. A removed setting that appears in godebug entries or //go:debug comments is accepted when set to the final default value, so old configurations keep building, and the build fails only when it asks for the old behavior that no longer exists:

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

$ go build            # go.mod contains: godebug asynctimerchan=0
$ echo $?
0

A selection of settings that shows the variety (the GODEBUG history on go.dev lists them all, by release):

Setting Since What it controls

panicnil

1.21

whether panic(nil) is allowed; panicnil=1 restores the pre-1.21 behavior

httpmuxgo121

1.22

the pre-1.22 ServeMux pattern syntax, without methods and wildcards

asynctimerchan

1.23, removed 1.27

restored the asynchronous, buffered timer channels of Go 1.22 and earlier instead of the synchronous ones of Go 1.23

randseednop

1.24

makes the global math/rand.Seed a no-op; randseednop=0 restores seeding

containermaxprocs, updatemaxprocs

1.25

whether GOMAXPROCS follows cgroup CPU limits and is updated periodically (Linux)

httpcookiemaxnum

1.26

maximum number of cookies net/http accepts when parsing a header (default 3000)

urlmaxqueryparams

1.26

maximum number of query parameters net/url accepts (default 10000)

tracebacklabels

1.26, default changed in 1.27

goroutine labels in tracebacks; the opt-out is expected to stay indefinitely

x509sslcertoverrideplatform

1.27

whether Windows and macOS load roots from disk when SSL_CERT_FILE or SSL_CERT_DIR are set; planned for removal in 1.31

httpservecontentmaxranges

added for 1.28, backported to 1.27.2 and 1.26.9

limit on the number of ranges in a Range header served by net/http file-serving functions (default 200)

The last row is a reminder of the policy for security fixes: the setting was added to protect against denial-of-service, so the Go team backported both the default and the opt-out to the supported releases even though Go 1.28 has not shipped.

GOEXPERIMENT: features in development

GOEXPERIMENT is a build-time variable that turns toolchain and runtime experiments on or off. It is read by go build, not at run time, and it is how the Go team ships a feature to early adopters before it is stable. Experiments are not covered by the compatibility promise; if you use one, label it in your own documentation.

The syntax is a comma-separated list. A name enables an experiment, a no prefix disables one, and none clears all experiment flags. An unknown name is an error:

$ go env GOEXPERIMENT

$ GOEXPERIMENT=simd go env GOEXPERIMENT
simd
$ GOEXPERIMENT=nogreenteagc go env GOEXPERIMENT
nogreenteagc
$ GOEXPERIMENT=none go env GOEXPERIMENT
none
$ GOEXPERIMENT=nosuchexp go env GOEXPERIMENT
go: unknown GOEXPERIMENT suchexp

The first command prints an empty line: with no experiment set, the baseline applies. The error message names suchexp, not nosuchexp, because the parser strips a leading no before it looks the name up. The build records the setting in the binary, so you can see afterwards how it was built, and each enabled experiment also becomes a build tag goexperiment.<name>:

$ GOEXPERIMENT=nogreenteagc,runtimesecret go build -o shelf-exp .
$ go version -m shelf-exp | grep -i exper
	build	GOEXPERIMENT=nogreenteagc,runtimesecret

The experiments that exist in go1.27.2, taken from the toolchain’s own internal/goexperiment flags:

Experiment Default in 1.27 What it does

greenteagc

on

the Green Tea garbage collector (default since Go 1.26; nogreenteagc opts out)

jsonv2

on

encoding/json/v2 and encoding/json/jsontext generally available, with encoding/json backed by v2; nojsonv2 restores the old v1 implementation, an opt-out expected to be removed in a future release

sizespecializedmalloc

on

size-specialized memory-allocation routines, about 1% faster in allocation-heavy programs at the cost of roughly 60 KB of binary size; the opt-out is expected to be removed in Go 1.28

regabiwrappers, regabiargs

on where supported

register-based calling convention (alias regabi); always on for amd64, arm64, loong64, ppc64, ppc64le, riscv64 and s390x

dwarf5

on, except darwin, ios and aix

DWARF version 5 debug information

randomizedheapbase64

on

heap base address randomization on 64-bit platforms

simd

off

the experimental simd package of portable, vector-size-agnostic SIMD types, and the amd64, arm64 and WebAssembly simd/archsimd package

runtimesecret

off

makes the runtime/secret package available

runtimefreegc

off

lets the runtime free and reuse memory more eagerly in some circumstances, with compiler help

mapsplitgroup

off

stores map groups as separate key and element arrays instead of interleaved slots

newinliner

off

a new version of the inlining phase of the compiler

arenas

off

makes the arena package visible

boringcrypto

off

builds the crypto packages on BoringCrypto

cgocheck2

off

an expensive cgo pointer-rule checker, regardless of GODEBUG=cgocheck

loopvar

off

per-iteration loop variables for all code (now the go 1.22 language rule)

heapminimum512kib

off

reduces the minimum heap size to 512 KiB

fieldtrack, preemptibleloops, staticlockranking

off

debugging and research flags of the compiler and runtime

Experiments come and go quickly. In Go 1.27 the goroutine leak profile graduated and its goroutineleakprofile flag was deleted; jsonv2, greenteagc and sizespecializedmalloc moved from opt-in to baseline. The Go 1.27 release notes are the reference for what is experimental in a given release: the new simd package and simd/archsimd need GOEXPERIMENT=simd.

Go telemetry

Go telemetry lets the Go team measure how the toolchain performs and is used, in a design aimed at transparency. It applies to programs maintained by the Go team, such as the go command, gopls and govulncheck, and not to programs you build.

  • Counters are lightweight counts of named events, written to a memory-mapped file on your computer when collection is enabled.

  • Reports are weekly aggregated summaries of approved counters; they are uploaded, and become publicly visible, only when you opt in.

  • Charts summarize uploaded reports for all users at telemetry.go.dev.

The behavior is controlled by one value, the mode, which is local by default: data is collected and kept on the local computer but never uploaded.

Command Mode

go telemetry

print the current mode

go telemetry on

collect data and, subject to sampling, upload reports

go telemetry local

collect on this machine only (the default)

go telemetry off

neither collect nor upload

$ go telemetry
local
$ go env GOTELEMETRY
local

go env GOTELEMETRY reports the mode and go env GOTELEMETRYDIR the directory with configuration and counter files, both read-only. The setting is per user and has no effect on the programs you compile.

Source: Go Telemetry

Platform minimums and architecture levels

New releases drop support for old operating systems, and microarchitecture variables let you raise the baseline of your own binaries. The official minimums, from the Go wiki, at the time of writing:

Platform Minimum

Linux

kernel 3.2 or later (Go 1.24 and later); WSL 1 is not supported, use WSL 2

Windows

Windows 10 or Windows Server 2016 and later (Go 1.21 and later)

macOS

macOS 13 Ventura or newer for Go 1.27 and later; macOS 12 Monterey for Go 1.25 and 1.26

iOS

iOS 12 or later

OpenBSD

the officially supported -stable releases

Solaris

illumos-based distributions or Oracle Solaris 11 and later

ppc64 and ppc64le

POWER8 and above; Go 1.27 changed the ppc64 Linux target ABI from ELFv1 to ELFv2 and added external linking, cgo and PIE support there (they need an ELFv2 user space)

go tool dist list lists every supported GOOS/GOARCH pair (47 in go1.27.2). The compiler can also target a minimum microarchitecture level, trading portability for speed, with environment variables read by go build:

Variable Values and meaning

GOAMD64

v1 (default), v2, v3, v4: each level adds instruction sets; v3 allows AVX2 and BMI, and binaries then fail to start on CPUs without them; no AVX-512 code is generated

GOARM64

v8.0 (default) to v8.9 and v9.0 to v9.5, optionally followed by ,lse or ,crypto; since Go 1.23

GOARM

5, 6, 7: software floating point, VFPv1 or VFPv3

GO386

sse2 (default) or softfloat

The build cache understands these variables, so changing one needs no cleanup. The deeper treatment of cross-compilation, build constraints and the GOOS/GOARCH matrix is in The go Command, Builds and Cross-Compilation.

Common pitfalls

  • Raising the go line casually. It changes language semantics (loop variables) and GODEBUG defaults, and it forces every consumer of a library to upgrade. Treat it as an API decision, and raise it only when you need the newer feature.

  • Confusing the minimum with the preferred toolchain. The go line is a floor; the toolchain line only says which toolchain to prefer when it is newer than yours.

  • Expecting GOTOOLCHAIN=local to honour the toolchain line. It does not switch; it only checks the go line.

  • Assuming CI downloads toolchains. Some CI images set GOTOOLCHAIN=local or block downloads, and then a go line newer than the image’s toolchain is an error; choose the behavior deliberately and pin the toolchain in the image.

  • Writing go 1.27.2 when go 1.27 is meant. The first form requires at least that patch release, which locks out contributors on earlier 1.27 patch releases for no reason.

  • Not testing against the oldest supported toolchain. The stdversion vet check helps, but a build with the oldest release your go line permits is the real test.

  • Relying on GODEBUG for new code. It is a bridge for old programs; each setting has a lifetime and a removal date.

  • Leaving an experiment in production. GOEXPERIMENT features have no compatibility promise.

  • Using go env -w GOOS=.... It changes every later build on the machine.

Where the books are outdated

Both books predate everything on this page apart from the compatibility promise:

Topic Book says Current approach

Several toolchains

Bodner ch. 1 shows installing a secondary toolchain with go get golang.org/dl/... and running it by its versioned name

the go and toolchain lines and GOTOOLCHAIN=auto download and run the toolchain a module asks for (Go 1.21); go install of the golang.org/dl tools is no longer necessary

The go line

the books predate Go 1.21, when the go directive was only advisory

since Go 1.21 it is a minimum requirement, the language version of the module and the key to GODEBUG defaults

Release cadence

Bodner ch. 1 describes the cadence and the compatibility promise

both still hold; this page lists the support policy (two newer releases) and the exceptions in the promise

Compatibility settings

neither book has GODEBUG, //go:debug or the godebug directive

all three exist (1.21, 1.21 and 1.23) and let new toolchains build old code with old behavior

Minimum platforms

Vivien ch. 1 lists very old operating-system minimums (such as Windows XP and OS X 10.7)

current minimums are Windows 10 or Server 2016 (since Go 1.21), macOS 13 (since Go 1.27) and Linux kernel 3.2 (since Go 1.24)

Initializing a module

Bodner ch. 9 uses go mod init and Vivien ch. 1 and 6 use a GOPATH with no module file

go mod init writes the go line of the running toolchain (go1.27.2 wrote go 1.27.2); the Go 1.26 release notes describe a lower default, so check, then set the version explicitly

Experiments

neither book discusses GOEXPERIMENT

the toolchain’s opt-in and opt-out flags (above), including those that gate new standard packages

Telemetry

neither book mentions it

a local-by-default mode, with go telemetry to change it (since Go 1.23)