skip to content

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

level: middleimportance: should knowfreq 44%

answer

  1. the flag just makes a word true
  2. present or absent, never a value
  3. an excluded file's declarations vanish
  4. write the opposite-constraint partner file
  5. the tag set covers dependencies too

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.

solid answer

~40 s

`-tags` takes a comma-separated list of arbitrary identifiers that are treated as true when the go command evaluates `//go:build` expressions, so `-tags integration` flips that file from excluded to included. Nothing else happens: a tag is not a constant, has no value, and cannot be read from Go code — if the tagged build needs a different value, declare it in the tagged file. The set of tags applies to the entire build, including dependency packages, so a library with its own `integration`-guarded files changes too. Because an excluded file's declarations do not exist, the usual pattern is a pair of files with opposite constraints — `//go:build integration` and `//go:build !integration` — each defining the same names, so exactly one is compiled either way and neither build has a missing or duplicate symbol.

code

go · 7 lines
go
//go:build integration

package storage

const backendAddr = "127.0.0.1:9000"

func usesRealBackend() bool { return true }

go deeper

for a junior

Be able to say that -tags integration makes that word true so the guarded file joins the build, and that without the flag the file is simply not compiled.

for a middle

Explain that tags are valueless identifiers, that exclusion removes declarations, and hence why the paired opposite-constraint file is the standard shape.

for a senior

Show judgment about tag proliferation: each tag doubles the configuration space, tags are global to the build including dependencies, and you verify the effect with a file listing rather than by eye.

for a principal

Decide which differences deserve a compile-time tag at all versus a run-time switch, and own the naming of any tag a library exposes, since that word becomes part of the contract every consumer's build shares.

## What the flag does `go build -tags a,b,c` adds the identifiers `a`, `b` and `c` to the set of build tags that are true while the go command evaluates build constraints. The same flag exists on `go test`, `go vet`, `go list` and the other build-flag commands, and it must be given consistently: a package built with one tag set and inspected with another is a different build. That is the whole feature. There is no registry of legal tags, no declaration, and no error for a name nothing uses — which is exactly why a typo is invisible. `-tags integratoin` succeeds and quietly builds the untagged configuration. ## Tags have no values A build tag is present or absent. There is no `-tags mode=fast`, no way to compare a tag inside an expression, and no way to read one at runtime from Go code. Engineers arriving from languages with a preprocessor look for the value form and there is not one. When a tagged build needs different data — an address, a limit, a feature switch — the data goes in the constrained file as an ordinary declaration: ``` //go:build integration package storage const backendAddr = "127.0.0.1:9000" ``` and the file with the opposite constraint declares the same constant differently. The rest of the package just uses `backendAddr` and never knows which file supplied it. ## The paired-file pattern Because exclusion removes declarations rather than disabling them, a single tagged file usually needs a partner: - `storage_integration.go` with `//go:build integration` - `storage_default.go` with `//go:build !integration` Both declare the same names. Whichever way the flag goes, exactly one is in the build, so there is never a duplicate definition and never an undefined one. If you write only the tagged half, the untagged build fails to compile the moment another file references what it defines — which is at least a loud failure. The quieter and worse case is a tagged file nothing else references: it drops out of the default build and stops being compiled, vetted or type-checked at all. ## Tags are global to the build The tag set is not scoped to your module. Every package compiled for this build, dependencies included, sees the same set. Two consequences follow. First, a tag name is effectively part of a public contract for a library: if a library documents an `integration` tag and your application uses the same word for something else, you get both behaviours at once. Prefer a distinctive name over a generic one for anything published. Second, adding a tag can *remove* files as well as add them, because a dependency may carry `//go:build !yourtag` files; a tag is a boolean input to every constraint in the build, not an additive flag. ## Choosing between a tag and a runtime switch A build tag is the right tool when the difference must not exist in the binary: code that must not link on a platform, an implementation that pulls in a heavy dependency, a file that only makes sense against a live external system. It is the wrong tool for anything that a user might want to change without rebuilding, and it is expensive in a subtler way — every additional tag doubles the number of configurations that would have to be compiled to prove the tree still builds. Most codebases can afford one or two meaningful tags; a codebase with six has combinations nobody has ever compiled. ## Reading the result rather than assuming it Because a mistyped or forgotten tag is silent, verify rather than assume: `go list -f '{{.GoFiles}}' -tags integration ./...` prints the files that are actually in the build for that tag set, and the same command without the flag prints the default set. The difference between the two is the exact effect of the flag, which is far more convincing than reading constraint lines by eye.

  • Why is a file with `//go:build integration` usually paired with one carrying `//go:build !integration`?
    Because exclusion deletes declarations rather than disabling them. If the package references a name that only the tagged file defines, the default build stops compiling. The partner file declares the same names with different bodies, so exactly one definition exists in either configuration and neither build is missing or duplicating a symbol.
  • How do you pass a value, not just a name, to a tagged build?
    You do not — build tags have no values. Put the value in the constrained file as an ordinary constant or variable, and let the opposite-constraint file declare the same name differently. If the value must vary per build rather than per configuration, that is a job for a linker flag or for configuration read at run time, not for a build tag.
  • Can adding a tag ever remove files from a build?
    Yes. The tag set is a boolean input to every constraint in the build, including those in dependencies, so any file guarded by `//go:build !yourtag` drops out when you set that tag. That is why tags are best kept few and distinctively named: a generic word can collide with a dependency's own meaning for it.

saying these in an interview costs you the question

  • Treats a build tag as a key-value define
  • Expects the tagged file to compile without the flag
  • Thinks a typo in -tags produces an error
  • Assumes tags apply only to the main module
  • Believes adding a tag can only add files