//go:build chooses which files are compiled¶
Level: 201 · anyone who has seen a _linux.go file and wondered who decides
One line: A //go:build line at the top of a file is a boolean expression over tags — fast, !fast, fast && !debug, linux, arm64, cgo — that the go command evaluates before it compiles anything: a file whose expression is false is not part of the package, so two files may define the same function under opposite constraints, and -tags, GOOS and GOARCH decide which one a build sees.
The module below defines pricing() twice: in pricing_fast.go behind //go:build fast and in pricing_careful.go behind //go:build !fast. A third file, audit.go, prints from an init function only under fast && !debug. The script runs the package three ways, then asks go list which files each build would use — including builds for platforms this machine is not, by setting GOOS and GOARCH by hand, so that the answer is the same on every machine that runs it.
Verified output of build_constraints_choose_files_sh.sh — regenerated by tools/run_examples.py, never hand-typed.
$ go mod init example
$ gofmt -l .
$ go run .
pricing: careful path, the default
$ go run -tags fast .
audit.go: compiled in, because fast && !debug
pricing: fast path, compiled in with -tags fast
$ go run -tags fast,debug .
pricing: fast path, compiled in with -tags fast
$ go list -f '{{.GoFiles}} ignored={{.IgnoredGoFiles}}' .
[main.go pricing_careful.go] ignored=[audit.go pricing_fast.go]
$ go list -tags fast -f '{{.GoFiles}} ignored={{.IgnoredGoFiles}}' .
[audit.go main.go pricing_fast.go] ignored=[pricing_careful.go]
$ GOOS=linux GOARCH=amd64 go list -f '{{.GoFiles}}' .
[clock_amd64.go clock_linux.go main.go pricing_careful.go]
$ GOOS=windows GOARCH=arm64 go list -f '{{.GoFiles}}' .
[clock_arm64.go clock_windows.go main.go pricing_careful.go]
$ GOOS=windows GOARCH=arm64 go list -tags fast -f '{{.GoFiles}}' .
[clock_arm64.go clock_windows.go main.go pricing_fast.go]
Reading the output¶
go run . printed the careful path; go run -tags fast . the fast one. Both files declare func pricing() string, which would be a redeclared in this block error if both were compiled; they never are, because fast and !fast cannot both hold. go list -f '{{.GoFiles}}' names the files a build would compile and {{.IgnoredGoFiles}} the ones its constraints excluded: with no tags, pricing_fast.go and audit.go are ignored; with -tags fast, pricing_careful.go is. Nothing was compiled for that answer — go list only evaluates the constraints.
fast && !debug is an expression, with Go's meaning. The audit line appeared under -tags fast and vanished under -tags fast,debug. A constraint may use ||, &&, ! and parentheses, and -tags takes a comma-separated list (go help buildconstraint ↗). A build satisfies the tags for its GOOS and GOARCH, unix on a Unix-like system, the compiler's name (gc), cgo when cgo is enabled, go1.N for every release up to the one in use, and whatever -tags adds. ignore is nobody's tag, so //go:build ignore is the idiom for a file that belongs to the directory but never to the package — a generator script, for instance (go generate runs a command you name).
A file name is a constraint too. clock_linux.go, clock_windows.go, clock_darwin.go, clock_amd64.go and clock_arm64.go carry no //go:build line; the suffixes _GOOS, _GOARCH and _GOOS_GOARCH are evaluated the same way (go/build ↗). Under GOOS=linux GOARCH=amd64 the build took clock_linux.go and clock_amd64.go; under GOOS=windows GOARCH=arm64 the other two — and still pricing_careful.go, or pricing_fast.go with -tags fast, because the two mechanisms combine. That is why the script sets GOOS and GOARCH rather than printing this machine's: the answer key has to match on the x86-64 Linux and arm64 Mac runners that check it.
Where the line goes. Before the package clause, preceded only by blank lines and other comments, and followed by a blank line so that it is not read as the package's documentation (go help buildconstraint ↗). Measured in this chapter: a //go:build after the package clause is refused by the compiler as misplaced compiler directive (the compiler errors) and reported by vet's buildtag analyzer as misplaced //go:build comment; without the blank line the constraint still holds, and gofmt inserts the line (what vet and gofmt catch). A line with a space, // go:build fast, is an ordinary comment: the file is compiled into every build and no tool says a word — the same page shows it.
go vet sees the same files¶
Vet type-checks the package a build would compile, so a mistake in a file behind a tag stays invisible until vet runs with that tag:
Verified output of build_constraints_vet_respects_tags_sh.sh — regenerated by tools/run_examples.py, never hand-typed.
$ go vet .
exit status 0
$ go vet -tags fast .
# example
# [example]
./report_fast.go:8:22: fmt.Printf format %s has arg orders of wrong type int
exit status 1
The printf analyzer found %s given an int — under -tags fast only. Every tag set that ships needs its own go vet and go test run; a CI job that runs them once, with no tags, has checked one build of several.
build_constraints_choose_files_sh.sh and build_constraints_vet_respects_tags_sh.sh
build_constraints_choose_files_sh.sh in full — pasted here by tools/run_examples.py from the file CI runs.
#!/usr/bin/env bash
# Two files define pricing() under opposite constraints, //go:build fast and
# //go:build !fast. Which one is compiled in depends on -tags, and go list
# shows the choice without building anything. The platform files at the end
# (_linux.go, _windows.go, _amd64.go, _arm64.go) are listed under a GOOS and
# GOARCH set by hand, so this script prints the same thing on every machine.
set -u
export GOTOOLCHAIN=local
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
cd "$dir" || exit 1
say() { printf '$ %s\n' "$*"; }
say 'go mod init example'
go mod init example >/dev/null 2>&1 || exit 1
cat >main.go <<'GO'
package main
import "fmt"
func main() {
fmt.Println("pricing:", pricing())
}
GO
cat >pricing_fast.go <<'GO'
//go:build fast
package main
func pricing() string { return "fast path, compiled in with -tags fast" }
GO
cat >pricing_careful.go <<'GO'
//go:build !fast
package main
func pricing() string { return "careful path, the default" }
GO
cat >audit.go <<'GO'
//go:build fast && !debug
package main
import "fmt"
func init() { fmt.Println("audit.go: compiled in, because fast && !debug") }
GO
say 'gofmt -l .'
gofmt -l .
say 'go run .'
go run .
say 'go run -tags fast .'
go run -tags fast .
say 'go run -tags fast,debug .'
go run -tags fast,debug .
say "go list -f '{{.GoFiles}} ignored={{.IgnoredGoFiles}}' ."
go list -f '{{.GoFiles}} ignored={{.IgnoredGoFiles}}' .
say "go list -tags fast -f '{{.GoFiles}} ignored={{.IgnoredGoFiles}}' ."
go list -tags fast -f '{{.GoFiles}} ignored={{.IgnoredGoFiles}}' .
# Platform files: the suffix is the constraint, so no //go:build line is needed.
rm audit.go
for name in clock_linux clock_windows clock_darwin clock_amd64 clock_arm64; do
printf 'package main\n' >"$name.go"
done
say "GOOS=linux GOARCH=amd64 go list -f '{{.GoFiles}}' ."
GOOS=linux GOARCH=amd64 go list -f '{{.GoFiles}}' .
say "GOOS=windows GOARCH=arm64 go list -f '{{.GoFiles}}' ."
GOOS=windows GOARCH=arm64 go list -f '{{.GoFiles}}' .
say "GOOS=windows GOARCH=arm64 go list -tags fast -f '{{.GoFiles}}' ."
GOOS=windows GOARCH=arm64 go list -tags fast -f '{{.GoFiles}}' .
build_constraints_vet_respects_tags_sh.sh in full — pasted here by tools/run_examples.py from the file CI runs.
#!/usr/bin/env bash
# go vet sees the same files the build would. A Printf mistake in a file
# behind //go:build fast is invisible to a plain go vet and found by
# go vet -tags fast, so a tag-gated file needs its own vet run.
set -u
export GOTOOLCHAIN=local
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
cd "$dir" || exit 1
say() { printf '$ %s\n' "$*"; }
go mod init example >/dev/null 2>&1 || exit 1
cat >main.go <<'GO'
package main
func main() {}
GO
cat >report_fast.go <<'GO'
//go:build fast
package main
import "fmt"
func report(orders int) {
fmt.Printf("orders: %s\n", orders)
}
GO
say 'go vet .'
go vet . 2>&1
echo "exit status $?"
say 'go vet -tags fast .'
go vet -tags fast . 2>&1
echo "exit status $?"
What to do¶
- Put
//go:buildon the first line that is not a comment, with a blank line after it, and letgofmtkeep it there. Never// go:build. - Use a file-name suffix for
GOOSandGOARCH, and a//go:buildline for what a suffix cannot say:unix,cgo, a Go version, or a tag of your own. - Make a pair complementary —
fastand!fast— so that exactly one definition exists under every tag set, then build, vet and test each tag set you ship with the same-tags. - Ask
go list -f '{{.GoFiles}}'before you ask CI. WithGOOSandGOARCHset, it lists the files of a build you cannot run here.
Practice¶
Without building anything, decide which of four targets would compile a file constrained by (linux && amd64) || (darwin && !cgo): linux/amd64, linux/arm64, darwin/arm64 with cgo, and darwin/arm64 with CGO_ENABLED=0. Use go/build/constraint, the package the go command itself uses, to parse the line and evaluate it against each target's tag set; then print the // +build spelling that gofmt would have kept for it before Go 1.17.
Solution
build_constraints_choose_files_kata_go.go in full — pasted here by tools/run_examples.py from the file CI runs.
// Kata: without building anything, decide which of four target builds would
// compile a file constrained by (linux && amd64) || (darwin && !cgo), using
// the same parser and evaluator the go command uses, go/build/constraint.
// Then print the old // +build spelling of the same expression.
//
// go build -trimpath build_constraints_choose_files_kata_go.go && ./build_constraints_choose_files_kata_go
package main
import (
"fmt"
"go/build/constraint"
"slices"
"strings"
)
func main() {
line := "//go:build (linux && amd64) || (darwin && !cgo)"
expr, err := constraint.Parse(line)
if err != nil {
panic(err)
}
fmt.Println("expression:", expr)
// The tags a build satisfies: GOOS, GOARCH, "unix" for Unix-like systems,
// the compiler, and "cgo" when cgo is enabled. Go 1.25 adds nothing here
// that would change the answers.
builds := []struct {
name string
tags []string
}{
{"linux/amd64, cgo on", []string{"linux", "amd64", "unix", "gc", "cgo"}},
{"linux/arm64, cgo on", []string{"linux", "arm64", "unix", "gc", "cgo"}},
{"darwin/arm64, cgo on", []string{"darwin", "arm64", "unix", "gc", "cgo"}},
{"darwin/arm64, CGO_ENABLED=0", []string{"darwin", "arm64", "unix", "gc"}},
}
for _, b := range builds {
compiled := expr.Eval(func(tag string) bool { return slices.Contains(b.tags, tag) })
fmt.Printf("%-28s compiled: %t\n", b.name, compiled)
}
lines, err := constraint.PlusBuildLines(expr)
if err != nil {
panic(err)
}
fmt.Println("the same constraint before Go 1.17:", strings.Join(lines, " "))
}
Verified output of build_constraints_choose_files_kata_go.go — regenerated by tools/run_examples.py, never hand-typed.
In other languages¶
- The Rust library's Cargo features ↗ and Conditional compilation ↗ — a feature is Rust's
-tags, but#[cfg(...)]sits on an item rather than on a file, so one file can hold both branches; and Cargo unifies features across every crate in a build, a problem Go does not have because a tag is a flag of one build. - C has no file-level constraint: the preprocessor's
#ifdef↗ with-DFASTdoes the job inside a file, and the build system decides which.cfiles to compile at all — the C library's Makefiles ↗ is that mechanism, written by hand.
Sources¶
- Build constraints, in the go command documentation ↗ (
go help buildconstraint), andgo/build, Build Constraints ↗ for the file-name suffixes. go/build/constraint↗ — the parser and evaluator the kata uses.- The Go 1.17 release notes ↗ —
//go:buildreplaces// +build— and the design draft ↗ that says why. - Jon Bodner, Learning Go, 2nd ed. (O'Reilly, 2024), chapter 11, "Go Tooling" — the sections "Using Build Tags" and "Building Go Binaries for Other Platforms".
- Alan A. A. Donovan and Brian W. Kernighan, The Go Programming Language (Addison-Wesley, 2015), chapter 10, "Packages and the Go Tool" — section 10.7.3, "Building Packages", on build tags.
- Miki Tebeka, Effective Go Recipes (Pragmatic, 2024), chapter 14, "Building Applications" — Recipe 75, "Using Build Tags for Conditional Builds".