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.
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.
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.
Related articles
- Go Go Modules and Package Management
A practical guide to Go modules: go.mod, go.sum, semantic versioning, replace directives, and the commands you actually need day to day.
- Go Go Tooling: go vet, gofmt, and go test
A practical guide to Go's built-in tooling trio: gofmt for formatting, go vet for static checks, and go test for unit, benchmark, and coverage workflows.
- Next.js Building a Next.js Monorepo with Turborepo
Set up a fast, cache-friendly Next.js monorepo using Turborepo. Share UI, config, and types between apps without sacrificing build performance.
- Go Go Context Cancellation Patterns
Master Go's context package: propagate deadlines, cancel goroutines safely, and avoid leaks with practical patterns for HTTP, database, and pipeline code.