skip to content

Build Constraints and Tags

Choosing which files compile for which platform or build flavor, either by filename suffix or an explicit //go:build expression. The usual interview use is gating slow integration tests behind a tag or swapping a platform-specific implementation.

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

questions

5

What does a `//go:build linux` line at the top of a Go file do, and where must it appear?

level: juniorimportance: must knowfreq 50%

answer

  1. a whole-file on/off switch
  2. the compiler never sees the file
  3. it has to come before package
  4. blank line keeps it out of the doc comment
  5. below the package clause it is just a comment

basics

~20 s

It is a build constraint. The file is compiled only when the target operating system is Linux, and is skipped entirely otherwise. The line must sit above the package clause, with a blank line after it.

solid answer

~40 s

`//go:build linux` is a build constraint: the go command includes that file in the build only when the target OS is Linux and drops the whole file otherwise, so the compiler never sees any declaration in it. The line must appear before the `package` clause, preceded only by blank lines and other `//` line comments, and is conventionally followed by a blank line so it is not mistaken for the package doc comment. The term after `//go:build` is a build tag: a GOOS value such as `linux`, a GOARCH value such as `arm64`, `cgo`, `race`, a `go1.N` release tag, or any identifier you pass with `-tags`. Written below the package clause it is just an ordinary comment and has no effect; `go vet`'s buildtag check reports that mistake.

code

go · 8 lines
go
//go:build linux

// Package watcher watches paths for changes.
package watcher

func platformName() string {
	return "linux"
}

go deeper

for a junior

Be ready to say in one sentence that the line decides whether the entire file is compiled, and to place it correctly: above the package clause, blank line after it.

for a middle

Explain the mechanics: the expression is boolean over tags, tags have no values, and an unsatisfied file's declarations vanish, which is why platform variants are written as one file per platform defining the same symbol.

for a senior

Show that you treat a silently excluded file as a real risk: it is code nobody compiles and therefore nobody type-checks, and you know go vet's buildtag check catches the misplaced or misspelled directive.

for a principal

Own the rule that every file in the repository must compile in at least one configuration the team actually builds, and be able to say who checks that and how, rather than leaving unbuildable files to rot in the tree.

## What a build constraint is A build constraint is a line comment of the form `//go:build <expression>` that decides, for one whole file, whether the go command hands that file to the compiler at all. It is not a conditional inside the code and it does not guard the lines below it: the unit of selection is the file. If the expression is false for the current build, the file behaves as if it were not in the directory — its functions, types, constants and `init` functions do not exist, and no error is reported for referring to something it would have defined only if nothing else in the package defines it either. The directive is spelled with no space after the slashes. `// go:build linux` is a plain comment and does nothing, which is one of the two classic ways to write a constraint that silently never applies. ## Where it goes The rules are mechanical: - It must appear **before the `package` clause**. - Only blank lines and other `//` line comments may precede it. - It should be **followed by a blank line**, which keeps it from being read as the package doc comment. Anything after the `package` clause is not a build constraint, no matter how it is spelled. The compiler will not complain — the file simply builds everywhere — so the failure is silent. `go vet` ships a `buildtag` analyzer that reports a misplaced `//go:build` comment, which is why running vet catches this class of mistake for free. ## What a build tag is A tag is a bare identifier. The go command sets a fixed set of them for every build: - the target OS, e.g. `linux`, `darwin`, `windows`, `js`, `plan9`; - the target architecture, e.g. `amd64`, `arm64`, `386`; - `unix` on the unix-like operating systems (recognised in `//go:build` expressions by recent Go); - `cgo`, when cgo is enabled and a C toolchain is available; - the compiler name, `gc` or `gccgo`; - `race`, `msan`, `asan` when those instrumentation modes are on; - `go1.1` through the release tag of the toolchain in use, so `//go:build go1.21` means "this toolchain or newer". On top of those, `-tags` adds whatever identifiers you list: `go build -tags integration` makes the term `integration` true. A tag has no value — it is present or absent. There is no `//go:build version==2` and no way to read a tag from Go code; anything that needs a value belongs in a constant declared in the constrained file itself. ## The expression The text after `//go:build` is a boolean expression over tags using `&&`, `||`, `!` and parentheses, exactly as you would write them in Go: ``` //go:build (linux || darwin) && !cgo ``` That file is compiled when the target is Linux or macOS and cgo is off. Because it is a real expression, it is possible to write one that is never satisfiable — `//go:build linux && windows` compiles nowhere — and nothing warns you. ## Why files rather than statements Go has no preprocessor and no `#ifdef`. The idiom is instead: declare the same function or type once per platform in separate files, each guarded by a constraint, plus a shared file with the platform-independent code that calls it. Every build sees exactly one definition, so there is no duplicate-symbol error and no dead branch inside a function. That is also why an unsatisfied constraint is more dangerous than it looks: it does not disable a feature, it removes a definition, and the reader on another OS sees a package that does not obviously contain the code they are reading about. ## The one-liner worth remembering `//go:build ignore` is a convention rather than a feature. Nothing ever sets a tag called `ignore`, so the file is excluded from every build while still sitting in the package directory, where gofmt and human readers can reach it. It is the standard way to keep a standalone helper program next to the package it belongs with without breaking the package build.

  • If the constraint is not satisfied, what happens to the declarations in that file?
    They do not exist for that build. The file is never parsed for compilation, so its types, functions, constants and `init` functions are absent. Other files that reference them fail to compile unless another file in the package supplies the same names under the opposite constraint. That is why platform-specific code is usually written as a set of files that each define the same symbol.
  • Which build tags are true without passing anything to -tags?
    The target GOOS and GOARCH values, `unix` on unix-like systems, `cgo` when cgo is enabled, the compiler name `gc` or `gccgo`, `race`/`msan`/`asan` under those instrumentation modes, and every `go1.N` release tag up to the toolchain in use. `-tags` only adds identifiers on top of that fixed set.
  • Why do people write `//go:build ignore` on a file they keep in the package directory?
    Because no build ever sets a tag named `ignore`, so the file is excluded everywhere. It is the conventional way to park a standalone helper program in a package directory without it joining that package's build. The tag is not special to the toolchain — any never-set identifier would behave the same; `ignore` is just the agreed name.

It is a switch on the file, not on the code inside it. Flip it off and the room is not dark, it is not in the house.

saying these in an interview costs you the question

  • Thinks the constraint only disables the lines below it
  • Puts the //go:build line after the package clause
  • Believes an excluded file still compiles but goes unused
  • Treats build tags as macros that carry a value
  • Writes // go:build with a space after the slashes
open as a page

How do filename suffixes like `_linux.go` or `_amd64.go` constrain when a Go file is compiled?

level: middleimportance: should knowfreq 46%

basics

~20 s

A Go filename ending in a known GOOS or GOARCH value, such as store_linux.go or store_amd64.go, carries an implicit build constraint for that target. The suffix only counts when the value is a real GOOS or GOARCH and something precedes the underscore.

open as a page

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

level: middleimportance: should knowfreq 40%

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.

open as a page

What does `go build -tags integration` change for a file whose header is `//go:build integration`?

level: middleimportance: should knowfreq 44%

basics

~20 s

It makes the term integration true for the whole build, so that file is compiled instead of skipped. Without the flag the file is excluded and anything it declares does not exist. A tag is only present or absent; it carries no value.

open as a page

A build-constrained Go file compiles in no configuration at all. How do you find which files a build actually includes, and why yours is missing?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Ask the go command instead of reading comments: go list -f '{{.GoFiles}}' prints the files in the build and go list -f '{{.IgnoredGoFiles}}' prints those a constraint excluded. Re-run it with the tags and target you expect, then check the constraint, the filename and the placement.

open as a page