Shipping Features Safely with Go Build Tags
Ship experimental Go features without breaking users. Zero runtime overhead, no feature flag infrastructure.
You've rewritten your login API. V2 is cleaner, faster, more secure. But shipping it means breaking every client. So it sits in a branch while V1 accumulates tech debt.
Build tags in golang help fix this. Ship both versions in production. Let teams migrate on their schedule. Zero runtime overhead, no feature flag infrastructure.
Quick Summary:
Build tags let you compile different code based on flags you pass to go build. Ship experimental features without breaking users. Roll out API v2 alongside v1. Zero runtime cost—the compiler removes unused code entirely.
The Problem
Your auth endpoint needs a v2. Better token handling, cleaner errors, proper rate limiting. But it breaks the existing API contract.
Your options:
- Ship to everyone → break production
- Run v1 and v2 services → double the infrastructure
- Use build tags → one binary, both versions
What About Runtime Feature Flags?
Runtime feature flags (LaunchDarkly, Unleash, etc.) are popular but come with tradeoffs:
- Network calls — fetch flag state from a service (adds latency to auth endpoints)
- Infrastructure — flag service, database, admin UI to maintain
- Runtime overhead — if-checks on every request that never get optimized away
- Cost — monthly SaaS fees or self-hosting burden
They're great when you need instant toggles without redeployment. But for rolling out breaking changes in libraries or services where you control deployment timing, build tags are simpler and faster.
This post focuses on build tags—Go's compile-time alternative with zero overhead.
What Are Build Tags?
Build tags are compile-time switches. You write two files with the same constant name, and Go picks exactly one based on flags you pass to go build. The unchosen code doesn't make it into your binary.
The Pattern
Viper (a popular Go config library) uses this two-file trick:
File 1: internal/features/finder.go
//go:build viper_finder
package features
const Finder = true
File 2: internal/features/finder_default.go
//go:build !viper_finder
package features
const Finder = false
How it works:
go build→ usesfinder_default.go(Finder = false)go build -tags viper_finder→ usesfinder.go(Finder = true)
Only one file makes it into the binary. The compiler sees the const and deletes dead code. No if-checks remain.
Why This Works
Zero Runtime Overhead
if features.Finder {
useNewFinder()
}
If Finder = false, the compiler removes this entire block. Nothing remains—not even the if-check.
Compare to runtime flags:
if config.EnableNewFinder { // checked every time
useNewFinder()
}
That if-check stays in the binary and runs every time. Build tags remove it completely.
Gradual Rollouts
v1.6: Ship with build tags
Default: stable version
Opt-in: go build -tags auth_v2
v1.7: New feature becomes default
Old version still available with tags
v2.0: Feature is standard
No one breaks. Early adopters test it first. You fix bugs before making it default.
A/B Testing
Build two binaries, deploy to different servers:
# Experimental version (10% of traffic)
go build -tags new_algorithm -o app-v2
# Stable version (90% of traffic)
go build -o app-v1
Compare metrics. Pick the winner.
Other Uses
Platform-Specific Code
// storage_windows.go
//go:build windows
func getConfigDir() string {
return os.Getenv("APPDATA")
}
// storage_unix.go
//go:build unix
func getConfigDir() string {
return filepath.Join(os.Getenv("HOME"), ".config")
}
Go picks the right one automatically based on your OS.
Smaller Binaries
// json_stdlib.go - 8MB binary
//go:build !compact
import "encoding/json"
// json_compact.go - 5MB binary
//go:build compact
import jsoniter "github.com/json-iterator/go"
Build with -tags compact to exclude heavy dependencies.
When to Use Them
Good for:
- Experimental features (opt-in with
-tags) - Platform-specific code (Windows vs Unix)
- Breaking API changes (ship v2 alongside v1)
- Smaller binaries (exclude heavy deps)
Bad for:
- Runtime config (env vars, config files)
- Things that change per deployment
- Features users need to toggle without rebuilding
Summary
Build tags let you ship risky changes safely. Two files, opposite tags, same const name. Compiler picks one, optimizes away the other.
Use them when you need to ship v2 of something critical without breaking v1 users. Or when you want early adopters testing new code before making it default.
No runtime cost. No infrastructure. Just cleaner rollouts.
Links: