skip to content

How does a `//go:build` expression differ from the legacy `// +build` line in syntax and precedence?

level: middleimportance: should knowfreq 40%

answer

  1. one is an expression, one is positional
  2. space and comma are not what you expect
  3. separate legacy lines are ANDed
  4. the newer line wins when both exist
  5. gofmt rewrites the legacy line for you

basics

~20 s

//go:build takes one ordinary boolean expression with &&, ||, ! and parentheses. The older // +build line uses spaces for OR, commas for AND, and repeated lines for AND. When a file has both, //go:build wins.

solid answer

~40 s

`//go:build` carries a single boolean expression written the way Go writes booleans: `//go:build (linux || darwin) && !cgo`. The legacy `// +build` line encodes the same thing positionally — a space means OR, a comma means AND, `!` negates one term, and multiple `// +build` lines are ANDed together — so `// +build linux darwin` plus `// +build !cgo` is the equivalent. Both must sit in the file header before the package clause and be followed by a blank line. If a file has both, the go command uses the `//go:build` line and ignores the `+build` lines, and gofmt keeps the legacy line in sync by rewriting it from the `//go:build` expression. The only reason to keep a `+build` line at all is a toolchain old enough to predate the `//go:build` form.

code

go · 5 lines
go
//go:build (linux || darwin) && !cgo
// +build linux darwin
// +build !cgo

package sysinfo

go deeper

for a junior

Know that two comment spellings exist, that //go:build is the one to write today, and that both must sit above the package clause.

for a middle

Decode the legacy grammar out loud — space is OR, comma is AND, repeated lines are ANDed — and state that the modern line takes precedence when both are present.

for a senior

Show you would fix drift with gofmt and go vet rather than by hand, and that you recognise a positional legacy line as a review hazard when the constraint is non-trivial.

for a principal

Decide whether the codebase still needs legacy lines at all, based on the oldest toolchain anyone actually builds with, and make the conversion a mechanical formatter-driven change rather than a hand edit per file.

## Two spellings of the same idea Go has had file-level build constraints for a long time, and for most of that time they were written as `// +build` lines. The syntax was compact but positional, and people got it wrong constantly. The `//go:build` form was added later as a plain boolean expression, in the same `//go:` directive family as `//go:embed` and `//go:generate`, and it is now the form you write. ## The legacy grammar, decoded A `// +build` line is a list of space-separated options; the file is built if **any** option matches. Each option is a comma-separated list of terms; an option matches if **all** its terms match. A term may be prefixed with `!` to negate it. And if the file has several `// +build` lines, **all** of them must be satisfied. So: - `// +build linux darwin` — linux OR darwin. - `// +build linux,386` — linux AND 386. - `// +build linux,386 darwin,!cgo` — (linux AND 386) OR (darwin AND NOT cgo). - two lines, `// +build linux darwin` and `// +build !cgo` — (linux OR darwin) AND NOT cgo. The two easy mistakes are reading the space as AND (it is OR) and forgetting that separate lines are ANDed. Both produce a file that compiles in the wrong set of builds, silently. ## The modern grammar `//go:build` takes one expression, using `&&`, `||`, `!` and parentheses with the usual precedence: ``` //go:build (linux || darwin) && !cgo ``` There is nothing positional left to misread, and parentheses let you write groupings the old form could not express directly. ## Placement, which is shared Both forms live in the file header: before the `package` clause, preceded only by blank lines and other line comments, and followed by a blank line so they are not absorbed into the package doc comment. Neither form does anything below the package clause — it is a plain comment there. `go vet` ships a `buildtag` analyzer that reports misplaced constraints and malformed lines, and it is the cheapest way to catch a constraint that is quietly inert. ## Precedence and gofmt When a file contains both forms, the `//go:build` line is authoritative and the `// +build` lines are ignored for the build decision. That rule matters because the two can drift apart during an edit: someone changes the expression on one line and not the other. gofmt closes that gap — it synchronises the pair, deriving the `// +build` lines from the `//go:build` expression, and adding a `//go:build` line to a file that has only the legacy form. `go vet` also reports a mismatch between them. In practice this means you edit the `//go:build` line and let the formatter deal with the rest; hand-editing a `+build` line is how the two end up disagreeing. ## What to do in a real codebase Write `//go:build` only. Keep a `// +build` line only if something in your world still builds the package with a toolchain that predates the newer form — and if you do keep it, never edit it by hand. When you inherit files carrying only the legacy syntax, running gofmt over the tree converts them mechanically, and the diff is reviewable because the boolean meaning is preserved by construction rather than by someone re-deriving it. ## The subtlety worth naming in an interview Because the legacy grammar is positional, a `+build` line cannot express every expression the new one can without splitting into multiple lines, and a negation applies to a single term only — there is no `!(a || b)` in the old form; you write `!a,!b`. That is the practical reason the newer syntax exists: the old one made complex constraints hard to write correctly and even harder to review.

  • What boolean expression does `// +build linux,386 darwin,!cgo` mean?
    (linux AND 386) OR (darwin AND NOT cgo). Commas bind terms into one option with AND, spaces separate alternative options with OR, and `!` negates the single term it prefixes. The equivalent modern line is `//go:build (linux && 386) || (darwin && !cgo)`.
  • If a file's `//go:build` and `// +build` lines disagree, which one decides the build?
    The `//go:build` line. The legacy lines are ignored for the build decision when a `//go:build` line is present, which is exactly why drift between them is easy to miss. gofmt regenerates the legacy line from the modern one, and `go vet` reports a mismatch, so the fix is to run the formatter rather than to hand-edit.
  • Is there any reason left to keep a `// +build` line in a new file?
    Only compatibility with a toolchain old enough not to understand `//go:build`. For anything building with a current toolchain the legacy line is redundant noise. If you do keep one, treat it as generated output: edit the `//go:build` expression and let gofmt write the other line.

saying these in an interview costs you the question

  • Reads the space in a +build line as AND
  • Thinks multiple +build lines are ORed together
  • Believes the legacy line overrides the //go:build line
  • Hand-edits the +build line and lets the two drift
  • Claims parentheses work in a +build line