skip to content

GODEBUG Settings

When a release changes behaviour, GODEBUG restores the old one: defaults follow your go line, a go.mod godebug directive pins them, and //go:debug sets them in main.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

5

What does the GODEBUG environment variable control, and how is its value formatted?

level: juniorimportance: should knowfreq 36%

answer

  1. an environment variable, not a compiler flag
  2. comma-separated pairs
  3. each name restores one older behaviour
  4. read when the process starts
  5. panicnil=1 is the classic example

basics

~20 s

GODEBUG carries comma-separated name=value settings, such as panicnil=1, that switch particular runtime and standard-library behaviours back to an older Go release's semantics. A Go binary reads them when it starts, so no rebuild is needed.

solid answer

~40 s

GODEBUG is a comma-separated list of `name=value` settings read by the Go runtime and parts of the standard library when a program starts: `GODEBUG=panicnil=1,asynctimerchan=1 ./myserver`. Each name is a documented switch that restores the behaviour of an earlier Go release for exactly one change — `panicnil=1` brings back the pre-Go-1.21 rule where `panic(nil)` panicked with a nil value instead of a `*runtime.PanicNilError`. It is not a debug-logging switch and it is not a general feature-flag system: the set of names is fixed by the Go release you built with, and each one is documented individually. Because it is an environment variable, you can flip it on a binary you already have, which makes it the fastest way to test whether a behaviour change in a new Go release is what broke you.

code

text · 5 lines
text
# restore the pre-Go-1.21 panic(nil) behaviour for one process
GODEBUG=panicnil=1 ./billing-worker

# two settings at once, no spaces
GODEBUG=panicnil=1,asynctimerchan=1 ./billing-worker

go deeper

for a junior

Recall the shape: GODEBUG=name=value, comma-separated, read at process start, one named setting per behaviour change. Have one concrete example ready, such as panicnil=1.

for a middle

Explain why the mechanism exists at all: Go ships a deliberate behaviour change and gives you a named, documented switch back to the old semantics without touching your source or your build.

for a senior

Show you would use it as a diagnosis tool during an upgrade — flip one setting on one instance to confirm which change broke you — and that you treat it as temporary, not a fix.

for a principal

Be ready to say who is allowed to set one in production, where it should be recorded so it is visible in review, and how the organisation gets rid of it before the release that deletes it.

## The problem GODEBUG solves Go promises that code which compiles and runs today keeps working on later toolchains. Sometimes, though, a change is worth making anyway — a bug fix, a security tightening, a semantic correction — and it will change what some existing program does at run time. GODEBUG is the escape hatch for exactly those changes: for each one, the Go team ships a named setting that restores the old behaviour, so that a team hit by the change can keep running while they fix their code. The name is misleading. Despite "DEBUG", it is not a verbosity knob and it does not turn on logging in general. It is a list of compatibility switches (plus a separate family of diagnostic knobs that belong to the performance-tooling discussion, not here). ## The format The value is a comma-separated list of `name=value` pairs, with no spaces: ``` GODEBUG=panicnil=1 ./billing-worker GODEBUG=panicnil=1,asynctimerchan=1 ./billing-worker ``` Most compatibility settings are boolean-ish: `1` means "behave the old way", `0` means "behave the new way". Which value is the default depends on the release and on how the program was built. The settings are consulted when the process starts, by the runtime and by the standard-library packages that own the affected behaviour. That is the property that makes GODEBUG operationally useful: you can take a binary that is already built, already signed, already deployed, and change one behaviour by changing the environment of the next process you start. Nothing is recompiled and nothing in your source tree changes. A name the running toolchain does not know is not an error you will notice — it simply has no effect. That matters later, because compatibility settings are eventually removed. ## A concrete example Before Go 1.21, `panic(nil)` panicked with a nil value, so `recover()` returned nil and a `if r := recover(); r != nil` guard treated the panic as if nothing had happened — a real source of silently swallowed failures. Go 1.21 changed `panic(nil)` to panic with a `*runtime.PanicNilError` instead, so `recover()` returns something non-nil. That is a better rule, and it is also a behaviour change: a program that relied on the old shape can be made to behave the old way again with `GODEBUG=panicnil=1`. Another: Go 1.23 changed the channels returned by `time.Timer` and `time.Ticker` so they are unbuffered and the timers can be collected earlier. Code that had been written around the old buffered channel could set `GODEBUG=asynctimerchan=1` to get the old behaviour back — until Go 1.27 removed that setting along with the old implementation. ## What GODEBUG is not - It is **not** a place to put your own application feature flags. The names are defined by the Go release; you cannot add one. - It is **not** a compiler switch. It changes what the already-compiled runtime and standard library do, not how your code is generated. The build-time equivalent, for experimental toolchain features, is `GOEXPERIMENT`, and that one does require a rebuild. - It is **not** permanent. Each compatibility setting exists for a documented window and is then deleted, so a program leaning on one is on a clock. - It is **not** the only way to set these values. A module can bake defaults into the build so they apply without anyone setting an environment variable, and the environment variable then overrides those baked-in defaults at run time. ## How you actually meet it The usual first encounter is an upgrade. You move a service from one Go release to the next, something behaves differently, and the release notes for that change name the GODEBUG setting that turns it off. Setting it on one instance is a cheap, reversible experiment that answers "is this change what broke us?" in one deploy — far cheaper than bisecting the whole upgrade. Once the answer is yes, the setting buys time; it does not substitute for fixing the code.

  • Does changing GODEBUG require rebuilding the binary?
    No. The settings are read by the runtime and standard library when the process starts, so you can flip one on a binary that is already built and deployed by changing the environment of the next process. That is what makes it a viable incident-time action. Rebuilding is only needed if you want the setting baked into the module so it also applies in tests and to anyone else who builds it.
  • Can you invent your own GODEBUG name for your application's feature flags?
    No. The set of names is defined by the Go release — each one corresponds to a specific documented behaviour change in the runtime or standard library. A name the toolchain does not recognise simply has no effect, so an invented setting fails silently. Application feature flags belong in your own configuration, not in GODEBUG.
  • What does panicnil=1 actually change?
    It restores the behaviour from before Go 1.21, where `panic(nil)` panicked with a nil value so `recover()` returned nil and the usual `r != nil` guard skipped the recovery path. From Go 1.21 the runtime panics with a `*runtime.PanicNilError` instead, so `recover()` returns a non-nil error value and the panic stops being invisible.

saying these in an interview costs you the question

  • Thinks GODEBUG turns on verbose runtime logging
  • Believes it is a general-purpose application feature-flag system
  • Says the binary must be recompiled for a GODEBUG change
  • Writes settings space-separated or one per variable
  • Assumes a GODEBUG setting is supported forever
open as a page

Which mechanisms let a Go module ship its own GODEBUG defaults instead of relying on the environment?

level: middleimportance: should knowfreq 28%

basics

~20 s

Two declarations bake GODEBUG settings into a build: a godebug line in go.mod (Go 1.23) and a //go:debug comment before the package clause of a main package or test file (Go 1.21). The GODEBUG environment variable still overrides both.

open as a page

How do you prove a service still needs its GODEBUG opt-out after a Go upgrade?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Read the runtime's /godebug/non-default-behavior counter for that setting: it increments only when the program actually took the old code path. Zero across a full traffic cycle, including rare batch paths, is the evidence to remove the setting.

open as a page

How does GOEXPERIMENT differ from GODEBUG when changing Go runtime or toolchain behaviour?

level: middleimportance: nice to knowfreq 24%

basics

~20 s

GODEBUG is read at run time and restores an older, documented behaviour of a shipped feature. GOEXPERIMENT is applied when code is compiled and selects experimental toolchain and runtime features, so it needs a rebuild and carries no compatibility guarantee.

open as a page

When is setting a GODEBUG opt-out in production the right call, and who owns removing it?

level: principalimportance: nice to knowfreq 26%

basics

~20 s

Prefer one named GODEBUG setting over rolling a Go upgrade back: it restores a single documented behaviour and keeps every other fix. Record it in the module, instrument it, and give removal a named owner and an upstream deadline.

open as a page