skip to content

What does the Go 1 compatibility promise explicitly leave itself free to change?

level: middleimportance: should knowfreq 45%

answer

  1. the document has an exceptions list
  2. if the spec never said it, don't rely
  3. security and bug fixes win
  4. tools were never in scope
  5. adding a field breaks positional literals

basics

~20 s

The promise excludes unspecified behaviour such as map iteration order and error message text, plus security fixes, bug fixes, tool behaviour and performance. Additive API changes are allowed too, and adding a struct field breaks unkeyed composite literals.

solid answer

~50 s

The document carves out several categories. Unspecified behaviour is the big one: map iteration order, which ready case a `select` picks, goroutine scheduling, collector timing and the exact text of error strings are not API, so code depending on them may break legally. Security fixes win over compatibility, and so do fixes to bugs in the compiler, runtime or libraries — Go 1.25 fixed a compiler bug that had been delaying nil-pointer checks, so code that silently "worked" started panicking. Tools were never in scope: `gofmt`'s output has changed, `go test` runs the `stdversion` vet check by default as of Go 1.27, and `go tool doc` was removed in Go 1.26. Finally, additive changes are allowed: a new struct field breaks unkeyed composite literals, a new method can break a type that embeds two others, and a new exported name can break a dot-import.

code

go · 10 lines
go
type Options struct {
	Name string
	Size int
}

// Unkeyed: must supply every field, so a new field breaks this line.
a := Options{"report", 10}

// Keyed: unaffected when the struct grows.
b := Options{Name: "report", Size: 10}

go deeper

for a junior

Know the headline exclusion: behaviour the language never specified — map iteration order is the classic — is not something you may rely on from one release to the next.

for a middle

Be able to name the categories and give one example of each: unspecified behaviour, security fixes, bug fixes, tooling, performance, and additive API changes such as a new struct field.

for a senior

Demonstrate that you audit for these before an upgrade: tests asserting error text, unkeyed struct literals, unsafe layout assumptions, and anything timing- or order-dependent.

for a principal

Set the standard that keeps upgrades cheap. Ban assertions on unspecified behaviour in the codebase so nobody has to argue about whether a toolchain upgrade may proceed.

## The exceptions are part of the policy The Go 1 compatibility document is short, and roughly half of it is a list of things the promise does **not** cover. Those exceptions are what make the promise keepable: without them Go could not fix a security hole, could not correct a compiler bug, and could not ever improve its tools. Knowing the list is what separates "Go doesn't break things" folklore from an engineer who can predict which of their own code is at risk. ## Unspecified behaviour If the language specification does not define an outcome, no release owes you the outcome you happened to observe. The classic cases in Go: - **Map iteration order.** The spec says the iteration order over maps is not specified, and the runtime randomises it deliberately so that nobody can accidentally depend on it. - **Which ready case a `select` chooses** when several are ready — the choice is pseudo-random by design. - **Goroutine scheduling order** and how work is spread across processors. - **Garbage-collection timing** and when finalisers or cleanups run. - **The exact text of error strings** returned by standard-library functions. Error *values* are matchable with `errors.Is` and `errors.As`; their wording is not API. - **The relative order of equal elements** under a non-stable sort such as `sort.Slice`. A test that asserts on any of these is a latent upgrade failure, and it is your bug, not Go's. ## Security fixes and bug fixes The document states that a security fix may break compatibility if it must. So may a fix to a bug in the specification, the compiler, the runtime or a library: once behaviour is agreed to be wrong, preserving it is not something the promise requires. Go 1.25 is the crisp example — a Go 1.21 compiler bug had been delaying nil-pointer checks, and fixing it meant some programs that had appeared to work started panicking at the dereference they always contained. Legal, documented in the release notes, and painful if your only reaction is "Go broke my code". ## Tools, performance and internals The toolchain is outside the promise entirely. `gofmt` may format differently — Go 1.19 reformatted doc comments across the ecosystem. `go vet` gains analysers, and `go test` runs some of them by default, so a build that passed can fail without your code changing; as of Go 1.27 `go test` runs the `stdversion` check. Commands can be dropped: `go tool doc` was removed in Go 1.26 in favour of `go doc`. Performance and memory are equally free — Go 1.24 moved maps to Swiss Tables and Go 1.26 made the Green Tea collector the default, both invisible to the documented API and both very visible in a benchmark. Similarly, the promise does not extend to programs that reach past the API: `unsafe` code that assumes a particular memory layout, or anything depending on internal or undocumented behaviour, is on its own by design. ## Additive changes that can still break you This is the subtle bucket. The promise allows the standard library to grow, and growth has three known breakages: 1. **A new field in an exported struct** breaks composite literals written without field names, because the literal no longer supplies every field. 2. **A new method on an exported type** can break a struct that embeds two types, if the new method makes a promoted-name collision ambiguous. 3. **A new exported package-level name** can collide in a file that uses a dot-import. All three are cheap to avoid: always write keyed struct literals, be wary of embedding two foreign types, and do not dot-import outside tests. ## Staying inside the promise The practical takeaway is a small code standard: keyed composite literals; match errors with `errors.Is`/`errors.As` rather than string comparison; never assert on map order, scheduling, or timing in a test; keep `unsafe` to a bounded, well-tested corner; and let CI tolerate tool-output changes rather than pinning a formatter forever. Code written that way upgrades on the promise; code that ignores it upgrades on luck.

  • Why is map iteration order randomised rather than merely undefined?
    Because undefined-but-stable would quietly become a dependency. Randomising the start of iteration makes the lack of a guarantee visible on the second run, so code that assumes an order fails early rather than years later during a toolchain upgrade. It is the promise's exceptions being enforced at runtime instead of in a document.
  • A toolchain upgrade makes a program panic on a nil pointer with no source change. Has the promise been broken?
    Almost certainly not. Fixes to compiler, runtime and library bugs are an explicit exception, and Go 1.25 fixed a Go 1.21 bug that had been delaying nil-pointer checks. The panic is your latent nil dereference finally being reported at the right place, so the fix belongs in your code.
  • Your CI diff fails after an upgrade because gofmt reformatted committed files. What is your standing?
    None, under the promise — tool behaviour is outside it, and gofmt's output has changed before. The right response is to reformat and commit, and to treat formatting drift as a normal cost of upgrading rather than freezing the formatter version indefinitely.
  • How do you keep your own code inside the part of the promise that is actually guaranteed?
    Write keyed composite literals, match errors with errors.Is or errors.As instead of comparing message text, never assert on map order, scheduling or timing, avoid dot-imports outside tests, and confine unsafe layout assumptions to a small well-tested area. That set covers nearly every real upgrade break.

saying these in an interview costs you the question

  • Assumes map iteration order is stable within one release
  • Says a compiler bug must be preserved once code depends on it
  • Thinks gofmt output and go vet checks are covered
  • Asserts on standard-library error message text in tests
  • Believes adding a struct field can never break callers