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 ( |
August and February, week 2 |
the release is issued, as |
After the release |
minor releases ( |
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.
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 |
|---|---|
|
release P of Go 1.N: |
|
release candidate R of Go 1.N, issued before |
|
the language version: the whole family of releases implementing that version of the language and standard library; |
|
a toolchain name: |
|
a toolchain name only for old releases ( |
|
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
Source: Go Toolchains: Go versions and go/version
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 |
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 |
|
Package |
programs that use it may depend on implementation details that change |
The |
outside the promise, because operating-system interfaces change; frozen since Go 1.4, with evolution in |
Sub-repositories |
code under |
The toolchain itself |
scripts that depend on the location or behavior of compilers, linkers and |
Performance |
no guarantee that a program does not get slower in a new release |
Features still in development |
unreleased and experimental features ( |
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 whosegoline is newer than itself, and a module’sgoline must be at least as new as thegoline 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 thegoline sets the minimum, thetoolchainline says what to prefer when you work in this module directly. A library may saygo 1.21so that others can depend on it with an old Go, andtoolchain go1.27.2so 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
}
Source: cmd/go: Build constraints
//go:build !go1.22
package main
func count() int {
n := 0
for i := 0; i < 3; i++ {
n++
}
return n
}
Source: cmd/go: Build constraints
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]
Source: Go Modules Reference: go directive, Go Toolchains: Module and workspace configuration and Go 1.22 release notes: language
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.)
Source: stdversion analyzer and Go 1.27 release notes: go test
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 |
|---|---|
|
sets |
|
requires the latest released toolchain |
|
|
|
removes the |
|
sets the |
|
changes only the |
|
if widget needs a newer Go, raises the |
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 |
|---|---|
|
always run the bundled toolchain, the one that shipped with this |
|
always run that exact toolchain: found in |
|
the default: start with the bundled toolchain and switch to a newer one when the module’s |
|
as |
|
the same, but starting from |
The selection rules, after reading go.work (or go.mod when there is no workspace) in the current directory, are exactly
these:
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; withGOSUMDB=offtoolchain downloads fail for lack of verification, andGOPRIVATEandGONOSUMDBpatterns 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+autois equivalent to replacing your Go 1.21 or newer installation with Go 1.27.2 as the default: the installedgocommand 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:
-
explicit
//go:debuglines in the program’smainpackage (Go 1.21); -
godebugdirectives in the main module’sgo.modor the workspace’sgo.work(Go 1.23); -
the defaults of the Go version in the
goline (and the special keydefault=goX.Yin either place); -
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
Source: runtime: PanicNilError and GODEBUG history: Go 1.21
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.modis consulted forgodebugdirectives; those in dependencies are ignored, and when a workspace is in usego.workreplaces thego.modfiles for this purpose; -
//go:debuglines go at the top of a file of a main package, before thepackageclause; in_test.gofiles they apply to the test’s main package; everywhere elsego vetreports them as misplaced; -
an unknown setting is an error in
godebugdirectives and in//go:debuglines, and so is listing the same setting twice in//go:debug; toolchains older than Go 1.23 rejectgodebuglines altogether, and older than 1.21 ignore//go:debug; -
a setting introduced for a security release applies the new behavior to all
golines.
$ 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 |
|---|---|---|
|
1.21 |
whether |
|
1.22 |
the pre-1.22 |
|
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 |
|
1.24 |
makes the global |
|
1.25 |
whether |
|
1.26 |
maximum number of cookies |
|
1.26 |
maximum number of query parameters |
|
1.26, default changed in 1.27 |
goroutine labels in tracebacks; the opt-out is expected to stay indefinitely |
|
1.27 |
whether Windows and macOS load roots from disk when |
|
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 |
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.
Source: GODEBUG history: Go 1.27 and runtime/metrics
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 |
|---|---|---|
|
on |
the Green Tea garbage collector (default since Go 1.26; |
|
on |
|
|
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 |
|
on where supported |
register-based calling convention (alias |
|
on, except darwin, ios and aix |
DWARF version 5 debug information |
|
on |
heap base address randomization on 64-bit platforms |
|
off |
the experimental |
|
off |
makes the |
|
off |
lets the runtime free and reuse memory more eagerly in some circumstances, with compiler help |
|
off |
stores map groups as separate key and element arrays instead of interleaved slots |
|
off |
a new version of the inlining phase of the compiler |
|
off |
makes the |
|
off |
builds the |
|
off |
an expensive cgo pointer-rule checker, regardless of |
|
off |
per-iteration loop variables for all code (now the |
|
off |
reduces the minimum heap size to 512 KiB |
|
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 |
|---|---|
|
print the current mode |
|
collect data and, subject to sampling, upload reports |
|
collect on this machine only (the default) |
|
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 |
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 |
|---|---|
|
|
|
|
|
|
|
|
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
goline casually. It changes language semantics (loop variables) andGODEBUGdefaults, 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
goline is a floor; thetoolchainline only says which toolchain to prefer when it is newer than yours. -
Expecting
GOTOOLCHAIN=localto honour thetoolchainline. It does not switch; it only checks thegoline. -
Assuming CI downloads toolchains. Some CI images set
GOTOOLCHAIN=localor block downloads, and then agoline newer than the image’s toolchain is an error; choose the behavior deliberately and pin the toolchain in the image. -
Writing
go 1.27.2whengo 1.27is 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
stdversionvet check helps, but a build with the oldest release yourgoline permits is the real test. -
Relying on
GODEBUGfor new code. It is a bridge for old programs; each setting has a lifetime and a removal date. -
Leaving an experiment in production.
GOEXPERIMENTfeatures 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 |
the |
The |
the books predate Go 1.21, when the |
since Go 1.21 it is a minimum requirement, the language version of the module and the key to |
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 |
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 |
|
Experiments |
neither book discusses |
the toolchain’s opt-in and opt-out flags (above), including those that gate new standard packages |
Telemetry |
neither book mentions it |
a |