skip to content

Gating Features in go.mod

The go line is a language version, not merely a floor: it decides whether newer syntax and semantics apply to your module, and //go:build go1.x narrows that to a single file.

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

questions

5

What does the `go 1.22` line in a module's go.mod declare, and is it the Go version you must have installed?

level: juniorimportance: must knowfreq 60%

answer

  1. one line, two separate jobs
  2. a floor, not the compiler you have
  3. minimum release plus language version
  4. newer compiler, still the old semantics
  5. go mod edit -go= raises it deliberately

basics

~20 s

The go line names the minimum Go release the module needs and the language version its files compile under. It is a floor, not the exact compiler you run: any newer Go release builds the module fine, and still applies the older language version.

solid answer

~40 s

The `go` directive in go.mod does two jobs. It is a minimum requirement: since Go 1.21 the go command treats a go line newer than the release doing the build as an error rather than compiling anyway. And it sets the language version for every file in the module, so language features introduced after that version are rejected even when a much newer release is compiling. It says nothing about which compiler you happen to have installed — Go 1.27 will happily build a `go 1.22` module and will keep giving it Go 1.22 language semantics. Raising it is a deliberate edit: `go mod edit -go=1.24`, or `go mod tidy -go=1.24` if you also want requirements reconciled in the same step.

code

mod · 5 lines
mod
module example.com/internal/telemetry

go 1.22

require example.com/other v1.4.0

go deeper

for a junior

Be ready to say the two things the line means in one breath: minimum Go release required, and the language version the module's files compile under. Knowing it is not the compiler you installed is the whole point.

for a middle

Explain the mechanics: which kinds of change are gated on it (language features) versus which are not (standard-library symbols, which come from the installed release), and name the commands that change it.

for a senior

Show the production angle. A go line that is too low hides new-API usage behind confusing undefined-symbol errors for consumers; one that is too high blocks builds. Know how to read every module's go line before you upgrade anything.

for a principal

Own the framing that the directive is a compatibility floor imposed on everyone downstream. Argue where the floor should sit for the modules your organisation publishes, and who is consulted before it moves.

## The one line, and its two jobs Every module has a `go.mod` file, and near the top it carries a directive like: ``` module example.com/internal/telemetry go 1.22 ``` That single line does two different things, and most confusion about it comes from collapsing them into one. ### Job 1: a minimum required release The go line says "this module needs Go 1.22 or later". Since **Go 1.21** that is a hard requirement: if the release doing the build is older than the go line, the go command reports an error naming the required version instead of attempting the compile, unless it is able to run a newer Go release instead. Before Go 1.21 the line was advisory — the toolchain compiled anyway and only mentioned the module's go version as a note appended to whatever compile error you got, which produced very confusing bug reports ("undefined: something" plus a version hint at the bottom). It is a **floor**, not a pin. Nothing stops a newer release from building the module; the whole point is that a Go 1.27 install can build modules whose go lines say 1.16, 1.21 or 1.25. ### Job 2: the language version The go line also sets the **language version** used to compile every file in the module. Language changes in Go are gated on it, so a module with a low go line does not silently pick up new syntax and semantics when someone builds it with a newer compiler. Concretely: - `min`, `max` and `clear` builtins arrived in **Go 1.21**; - per-iteration loop variables and `for range` over an integer arrived in **Go 1.22**; - range-over-function iterators arrived in **Go 1.23**; - generic type aliases arrived in **Go 1.24**. A module whose go.mod says `go 1.20` gets none of those, no matter how new the installed compiler is. Reach for one and the compiler rejects it with a message that names the language version it was given and points you back at go.mod. This is what makes the go line a *feature gate*. It exists so that upgrading your compiler is a safe, boring operation: the language your existing code is compiled as does not move until you move it. ### What the go line is not - **Not the compiler version you installed.** Those are independent; the installed release just has to be at least as new. - **Not a version of your dependencies.** Each dependency records its own go line and is compiled at its own language version. - **Not necessarily a per-file setting.** It applies module-wide, although a `//go:build go1.x` constraint can raise the language version for a single file. ### A standard-library subtlety worth knowing The go line gates *language* features. Standard-library **symbols** come from whatever release compiles the code, so a `go 1.20` module that calls a function introduced in Go 1.24 will compile happily on a Go 1.24 install and fail with a plain "undefined" error for anyone on Go 1.22 — a much worse error than a version requirement. Since **Go 1.27** `go test` runs the `stdversion` vet check by default, which reports uses of standard-library symbols that are too new for the module's effective Go version, turning that trap into a clear diagnostic. When you start depending on new stdlib APIs, raise the go line so consumers get an honest version error instead. ### Changing it `go mod edit -go=1.24` rewrites just the directive. `go mod tidy -go=1.24` sets it and reconciles the module's requirements to what that version implies. Either way, raising it is a decision with other people attached: every consumer of your module inherits the floor.

  • Does building a `go 1.20` module with Go 1.27 give it any Go 1.22 language features?
    No. The language version comes from the module's go line, so the code compiles under Go 1.20 rules: no per-iteration loop variables, no `for range` over an integer, no `min`/`max` builtins. The newer compiler only means the module is allowed to build at all; it does not upgrade the semantics.
  • What changed about the go directive's status in Go 1.21?
    It became a hard minimum. From Go 1.21 on, a go line newer than the release doing the build is reported as a version error rather than being treated as advice. Before that the toolchain compiled anyway and only appended a note about the required version when compilation happened to fail.
  • What is the difference between `go mod edit -go=1.24` and `go mod tidy -go=1.24`?
    `go mod edit -go=1.24` mechanically rewrites the directive and touches nothing else. `go mod tidy -go=1.24` sets the same directive and then reconciles the module's requirement list to what that language version implies, rewriting go.mod and go.sum as needed. Use edit for a surgical change, tidy when you want the file consistent afterwards.

It is like a document saying "requires Word 2016 or later, formatted for Word 2016": a newer editor opens it, but it is still laid out by the older rules until someone re-saves it.

saying these in an interview costs you the question

  • Says the go line pins which compiler version is used
  • Thinks a newer Go install automatically enables newer language features
  • Believes the go line is only documentation with no effect
  • Confuses the go directive with a dependency's version
  • Assumes every dependency compiles at the main module's language version
open as a page

A module with `go 1.21` in go.mod is built by Go 1.25 — does `for i := 0; i < 3; i++ { go func() { fmt.Println(i) }() }` share one `i`?

level: middleimportance: should knowfreq 42%

basics

~20 s

Yes, one shared i. Per-iteration loop variables arrived in Go 1.22 and are gated on the module's go line, not on the installed release. A go 1.21 line keeps Go 1.21 semantics, so all three goroutines close over the same variable.

open as a page

After a dependency upgrade, go build fails saying a module requires go >= 1.24 while CI runs Go 1.22 — why, and what are your options?

level: seniorimportance: should knowfreq 38%

basics

~20 s

A dependency you pulled in declares a go line of 1.24, and every module contributing packages to the build must be buildable by the release doing the build. The upgrade may also have raised your own go directive to match. Read the go.mod diff, then either move CI forward or step the dependency back.

open as a page

You maintain a shared internal library — how do you decide when to raise its go.mod go line, given every consumer inherits that floor?

level: principalimportance: should knowfreq 30%

basics

~20 s

Treat the go line as a compatibility floor you impose on people who did not ask for it. Decide from consumer evidence: who builds this library, what release they can run, and what the raise actually buys. Announce it, ship it in a normal release, and keep testing the oldest release you claim to support.

open as a page

How does a `//go:build go1.21` line let one file use a newer language feature while the package still builds on older releases?

level: middleimportance: nice to knowfreq 25%

basics

~20 s

A //go:build go1.21 constraint does two things: it excludes the file from builds by older Go releases, and it raises that one file's language version to 1.21 even when the module's go line is lower. A sibling file constrained with //go:build !go1.21 supplies the fallback.

open as a page