Skip to content
C Codeloom
Go

Go Build Tags Explained

Use Go build tags to include or exclude files per OS, architecture, or custom condition. Learn the new //go:build syntax, common patterns, and how tags interact with the test runner.

·4 min read · By Codeloom
Intermediate 7 min read

What you'll learn

  • What build tags do and where they go
  • The new //go:build expression syntax
  • Per-OS and per-arch file selection
  • Custom tags for integration tests
  • Tag interaction with go test and go run

Prerequisites

  • Comfortable with multi-file Go packages

Build tags are how Go decides which files belong in the build for a given target. They are simple comments at the top of a file, but they unlock cross-platform code, integration test toggles, and feature flags at compile time. Understanding them removes a lot of mystery from the standard library too.

What build tags are and why

A build tag is a constraint, declared with a //go:build line, that tells the Go toolchain whether to include a file. If the expression evaluates to true for the current target, the file compiles. If not, it is skipped as if it did not exist.

The “why” is conditional compilation without the C preprocessor’s pain. You can keep _linux.go and _windows.go next to each other, or guard expensive integration tests behind a custom tag so they only run in CI.

Mental model

Tags act like a boolean filter on each source file. The filter has three inputs: the operating system (GOOS), the architecture (GOARCH), and any extra tags passed via -tags. The Go compiler also adds implicit tags for the Go version (go1.21, etc.).

Filename suffixes are a shorthand. A file named cache_linux.go is implicitly tagged linux, with no comment needed. The explicit //go:build form is for everything that the suffix shorthand cannot express.

Hands-on example

A custom tag guarding a slow integration test.

//go:build integration

package store_test

import "testing"

func TestRealPostgresRoundTrip(t *testing.T) {
    // talks to a real database
}

Run normal tests with go test ./.... Run the integration set with go test -tags=integration ./.... The file simply does not exist for the default build.

Per-OS selection without filename suffixes.

//go:build linux || darwin

package fsutil

func enableFastPath() { /* uses syscalls available on unix */ }

Combine constraints with boolean operators.

//go:build (linux || darwin) && amd64 && !race

The line must appear before the package clause, followed by a blank line.

How the toolchain decides whether to include a file

Common pitfalls

The most common mistake is missing the blank line between the //go:build line and the package declaration. Without it, the compiler treats the comment as a regular doc comment and ignores it. Your file gets included on every platform.

The old // +build syntax still works for backwards compatibility but is deprecated. gofmt will keep both in sync if both are present, but you should write only //go:build in new code. Mixing them with conflicting expressions causes confusing inclusion rules.

Filename suffixes silently override your intent. A file called worker_linux.go is linux-only no matter what //go:build line you write. If you want a custom tag on a file with an OS suffix, rename the file.

Custom tags do not propagate to dependencies. Setting -tags=integration only affects your own packages and any vendored packages that also check that tag. Library authors must explicitly support a tag for it to do anything in their code.

Practical tips

Use custom tags for optional features: redis, prometheus, cgo_sqlite. Document them in your README so users know which -tags enable what.

For CI, keep a tag like integration or e2e and run two test passes: one without the tag for fast feedback, one with it for the full battery. This is far cleaner than a runtime environment variable check inside TestMain.

Avoid stacking too many tags on one file. If you need linux && amd64 && cgo && !race && go1.22, you probably want to split the logic across smaller files with clearer constraints.

The go list -f '{{.GoFiles}}' ./... command shows which files are selected for the current build. Add -tags=foo to verify your constraints actually do what you think.

Wrap-up

Build tags are a small feature with a large payoff. Remember the blank line, prefer //go:build over the deprecated form, lean on filename suffixes for OS and arch, and reach for custom tags whenever a file should only sometimes be compiled. With those rules internalized, conditional compilation becomes routine.