skip to content

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

level: middleimportance: should knowfreq 28%

answer

  1. two places besides the environment
  2. one lives in go.mod
  3. the other is a directive comment
  4. main packages and test files only
  5. the environment still wins at run time

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.

solid answer

~50 s

A module can declare settings rather than depend on whoever starts the process. In `go.mod`, a `godebug` line — `godebug panicnil=1`, or a factored `godebug (...)` block, or `godebug default=go1.21` to move every compatibility default at once — applies when main packages in that module are built (Go 1.23 and later). In a main package or a `_test.go` file, a `//go:debug panicnil=1` comment in the block immediately before the `package` clause does the same for that binary, and has been available since Go 1.21. Both are compiled into the binary as its defaults, so they apply in `go test` and on every machine that runs it, and both show up in code review. Precedence runs outward: the `GODEBUG` environment variable at run time beats `//go:debug`, which beats the `go.mod` `godebug` lines, which beat the defaults implied by the module's language version.

code

mod · 8 lines
mod
module example.com/billing

go 1.24

godebug (
	default=go1.23
	panicnil=1
)

go deeper

for a junior

Know that the setting does not have to live in the environment: a Go module can declare it in go.mod or in a //go:debug comment, and the environment variable still overrides both.

for a middle

Explain the two declaration forms precisely, including where a //go:debug comment is legal, and be able to order the four precedence levels from module baseline up to the environment variable.

for a senior

Argue for declaring the setting in the repository so tests and CI run with the same behaviour as production, and for a single named setting over a blanket default=go1.N.

for a principal

Own the convention: which form your organisation standardises on, how a setting gets reviewed when it lands, and what evidence is required before one may be added to a shared module.

## Why declare settings instead of exporting a variable Setting `GODEBUG` in a deployment manifest works, but it has a nasty property: the behaviour of the program now depends on something that is not in the repository. Local runs, `go test`, CI and production disagree, and the one place a reviewer looks — the diff — says nothing. Go therefore lets a module carry its own defaults, compiled into the binary. ## The go.mod godebug directive Since Go 1.23, `go.mod` (and `go.work`) accepts `godebug` lines: ``` module example.com/billing go 1.24 godebug ( default=go1.23 panicnil=1 ) ``` A single setting can also be written on one line: `godebug panicnil=1`. Two forms matter. `godebug name=value` pins one named setting. `godebug default=go1.N` moves *every* compatibility default to what Go 1.N did — a blunt instrument, useful when you are stepping a language version forward and want to separate "compile against the newer toolchain" from "adopt all its behaviour changes", and dangerous as a permanent posture because it hides which single behaviour you actually depend on. These lines take effect when main packages in that module are built. A `godebug` line in a *dependency's* `go.mod` does not reach into your program: it is the main module that decides. ## The //go:debug directive Since Go 1.21 a directive comment can set a default for one binary: ```go // Command billing-worker settles invoices. // //go:debug panicnil=1 package main ``` The rules to remember: it must be in the comment block immediately preceding the `package` clause, and it is only permitted in a main package or in a test file. That restriction is deliberate — a library has no business dictating run-time behaviour for the programs that import it, so the directive is confined to the artefacts that actually become a binary or a test binary. Putting one in an ordinary library package is a build error rather than a silent no-op. ## The baseline underneath both Underneath these declarations there is a baseline: the compatibility defaults a build starts from track the language version the main module declares, so that upgrading the toolchain without changing that line does not silently adopt every new behaviour at once. The mechanics of that language-version line itself are a separate topic; what matters here is that it is the bottom of the stack, and both `godebug` and `//go:debug` sit above it as explicit overrides. ## Precedence, from weakest to strongest 1. Defaults implied by the main module's declared language version. 2. `godebug` lines in the main module's `go.mod` (or `go.work`). 3. `//go:debug` directives in the main package or test file being built. 4. The `GODEBUG` environment variable at run time. The environment variable winning is the important end of that list. Whatever a module bakes in, an operator can still override it on a running deployment — which is what makes GODEBUG usable during an incident, and also means a compiled-in setting is a default, never a guarantee. ## Which one to use Use the `go.mod` `godebug` line when the whole module needs the setting — several commands built from one repository, and you want the tests to see it too. Use `//go:debug` when only one command needs it, or when the setting belongs to a test binary while production code stays on the new behaviour. Use the environment variable for experiments and for incident response, then promote the result into the repository so the next person can see it. Reserve `default=go1.N` for a deliberate, time-boxed step of the language version, and prefer a single named setting whenever you know which behaviour you are actually depending on. ## What this buys you A declared setting is testable. `go test` builds the test binary with the module's `godebug` lines applied, so a test can pin the behaviour you depend on and fail loudly the day someone removes the declaration. An environment-only setting has no such property: the test suite runs on the new behaviour and passes, and production runs on the old one.

  • Why is //go:debug rejected in an ordinary library package?
    Because a library would otherwise change run-time behaviour for every program that imports it, invisibly and possibly in conflict with another dependency. The directive is confined to main packages and test files — the artefacts that actually become a binary — so the decision stays with whoever owns the program. A library that needs old behaviour must ask its users to set it, or fix itself.
  • A binary is built from a module whose go.mod says godebug panicnil=1, but it is started with GODEBUG=panicnil=0. Which wins?
    The environment variable, so the program runs with the new behaviour. Compiled-in declarations are defaults; the run-time value overrides them. That ordering is deliberate: an operator must be able to change the behaviour of a binary they cannot rebuild, which is exactly the situation during an incident.
  • When is godebug default=go1.N a worse choice than naming one setting?
    Almost always, as a long-lived state. It freezes every compatibility behaviour at that release at once, so nobody can tell from the file which single behaviour the code actually relies on, and the eventual unfreeze changes many things simultaneously. It is defensible only as a deliberate, time-boxed step while you work out which named setting you really need.
  • Does a godebug line in a dependency's go.mod affect your program?
    No. Only the main module's go.mod (or the go.work file of the workspace being built) contributes godebug settings to a build. A dependency cannot reach up and change the run-time behaviour of the program that imports it, which keeps the decision with the module that produces the binary.

saying these in an interview costs you the question

  • Puts //go:debug in a library package and expects it to apply
  • Thinks a compiled-in setting cannot be overridden at run time
  • Believes a dependency's godebug line affects the final binary
  • Places //go:debug inside func main instead of before the package clause
  • Treats default=go1.N as equivalent to pinning one setting