Compiler & Linker Flags — Middle¶
1. The package pattern¶
go build -gcflags='all=-N -l' ./cmd/app # all packages
go build -gcflags='main=-N -l' ./cmd/app # only main
go build -gcflags='example.com/proj/...=-m' . # subtree
The pattern=flags form selects which packages get the flags. Useful when you want to debug only your code and let stdlib be optimized.
2. Common -gcflags¶
| Flag | Effect | When |
|---|---|---|
-N | Disable optimizations | Debugger compatibility |
-l | Disable inlining | Source-level debugging |
-m, -m=2, -m=3 | Print compiler decisions | Escape/inline diagnostics |
-S | Print assembly | Performance investigation |
-d=<key>=<value> | Debug knob | Specific runtime features |
-spectre=all | Spectre mitigations | Hardened environments |
-trimpath | Remove paths | Reproducible builds |
-gcflags='all=-N -l' is the standard "delve-friendly build" combo.
3. Common -ldflags¶
| Flag | Effect |
|---|---|
-s | Omit symbol table |
-w | Omit DWARF debug info |
-X pkg.var=val | Inject string into a string var |
-B id | Set build ID |
-linkmode=internal\|external | Pure-Go vs system linker |
-extldflags="..." | Pass flags to the external linker |
-r path | rpath for dynamic linking |
-X only works on declared var of type string. Constant strings can't be injected.
4. Reproducible builds¶
go build -trimpath -buildvcs=false \
-ldflags="-buildid='' -s -w -X main.ver=v1.2.3" \
-o app ./cmd/app
To get the same binary across machines:
-trimpath: remove file system paths.-buildvcs=false: skip git metadata embedding.-buildid='': don't include a random build ID.- Pin Go toolchain version (e.g.,
go.mod'sgo 1.24.2). - Build with identical
GOOS/GOARCH/GOMOD/CGO_ENABLED.
Verify with sha256sum of the binary on two machines.
5. Build modes¶
go build -buildmode=c-shared -o libapp.so . # for C to call Go
go build -buildmode=pie -o app . # position-independent
go build -buildmode=plugin -o plugin.so . # plugin (Linux/macOS)
Each changes the output format:
exe(default): standard executable.pie: hardened, address-randomization-friendly.c-archive/c-shared: for cgo embedding.plugin: for thepluginpackage.
6. Profile-guided optimization¶
# Step 1: capture a profile (e.g., from production or load test)
curl http://localhost:6060/debug/pprof/profile?seconds=60 > default.pgo
# Step 2: rebuild with the profile
go build -pgo=auto -o app ./cmd/app
The compiler uses the profile to make better inlining and devirtualization decisions. Typical gains: 2–10% CPU. PGO support is GA in Go 1.21+.
For multi-binary projects, place default.pgo next to main.go. For external profiles, use -pgo=/path/to/file.
7. Stripping symbol info for size¶
After stripping:
- Stack traces still readable (function names embedded in
runtime). delveand other source-level debuggers won't work.go tool nmproduces less info.- Binary is 10–20% smaller.
For production releases where you don't need on-the-fly debugging, strip; for staging/dev, don't.
8. Build cache and flags¶
Each combination of flags creates a separate cache entry. Switching between two flag sets repeatedly:
- First switch: cold cache, full rebuild.
- Second switch back: cache is warm, fast.
- Long-term flag stability minimizes cache thrash.
Tip for CI: build with the exact same flag string every time. Variations (e.g., a date in -X) cause unnecessary cache misses.
9. The -x and -work debugging duo¶
Output:
WORK=/tmp/go-build1234567
mkdir -p /tmp/go-build1234567/b001/
...
/usr/local/go/pkg/tool/darwin_arm64/compile -o ... main.go
...
You see the exact compile/link commands. -work keeps the workdir so you can inspect the intermediate files.
Use case: "why is my build picking up an old file?" Step through the printed commands.
10. go env GOFLAGS¶
Sets a default for every subsequent go build. Be careful — this affects every invocation including go test.
Better: write a Makefile or shell function that bundles flags explicitly.
11. Cross-compilation¶
The env vars select the target. Build flags work the same. For cgo, you need a cross-compiler.
Multi-target script:
for goos in linux darwin windows; do
for goarch in amd64 arm64; do
out="bin/app_${goos}_${goarch}"
[[ $goos == windows ]] && out="${out}.exe"
GOOS=$goos GOARCH=$goarch go build -ldflags="-s -w" -o $out ./cmd/app
done
done
12. Inspecting a built binary¶
go version -m ./app # build info
go tool nm ./app | head # symbols (none if -s)
go tool objdump ./app | head # disassembly
file ./app # static/dynamic, ELF/Mach-O
ldd ./app # dynamic deps (Linux)
go version -m is gold for figuring out "was this built with cgo? what tags? what go version?"
13. Common combos¶
# Production release
go build -trimpath -ldflags='-s -w -X main.ver=v1.0.0' -o app ./cmd/app
# Development (debugger-friendly)
go build -gcflags='all=-N -l' -o app ./cmd/app
# Show escape decisions
go build -gcflags='-m=2' ./pkg
# Race-detect testing
go test -race ./...
# PGO release
go build -pgo=default.pgo -trimpath -ldflags='-s -w' -o app ./cmd/app
# Static cgo (Alpine)
CGO_ENABLED=1 go build -ldflags='-linkmode=external -extldflags="-static"' -o app ./cmd/app
# WASM
GOOS=js GOARCH=wasm go build -o app.wasm ./cmd/web
Memorize the two or three you use daily; reach for the docs for the rest.
14. Summary¶
go build's flag system is small but expressive: -gcflags for the compiler, -ldflags for the linker, plus a handful of top-level switches like -trimpath, -pgo, -race. Cache awareness, package patterns, and the -x/-work debug duo round out the toolkit. Most projects can settle on 2–3 standard incantations and forget the rest.
Further reading¶
go help build,go help buildflagsgo tool compile -h,go tool link -h- PGO docs: https://go.dev/doc/pgo