Linking, Build Modes, and Race ABI
Linking, Build Modes, and Race ABI
Overview
After compilation, the linker stitches objects, resolves symbols, and emits an executable or library. Build modes and sanitizers change the ABI and cost model — important for plugins, cgo, and race-enabled CI.
Diagram: Build modes
go build
├── exe (default)
├── pie
├── c-shared / c-archive
└── plugin (limited platforms)
go build -race ──► instrumented memory + HB
Default Build
go build -o app .
go install ./cmd/appStatic-ish binaries are common on Linux with pure Go (no cgo). The Go runtime issues syscalls itself, so CGO_ENABLED=0 produces a fully static ELF with no libc.so. Enable cgo and glibc vs musl comes back: a Ubuntu-linked binary fails on Alpine because the ELF interpreter (/lib64/ld-linux-x86-64.so.2) is missing. See Go as a Systems Language.
go env CGO_ENABLED
CGO_ENABLED=0 go build -o app . # force pure Go when possible
file app && ldd app # Linux: expect "statically linked"Build Modes
go help buildmode| Mode | Use |
|---|---|
exe |
Default executable |
c-shared |
C shared library from Go |
c-archive |
C archive |
plugin |
Go plugin (limited platforms; fragile for prod) |
pie |
Position-independent executable |
go build -buildmode=pie -o app .Plugins sound attractive and usually disappoint: version skew with main binary, limited platforms, hard ops story. Prefer explicit interfaces + separate processes.
Linker Flags
go build -ldflags='-s -w' -o app . # strip symbols / DWARF
go build -ldflags='-X main.version=1.2.3' .-s -w smaller binaries, harder debug. Keep symbols in staging if you need crash analysis.
Race Detector
go test -race ./...
go build -race -o app-race .Race builds:
- Use a different memory allocator instrumentation
- Cost CPU and memory (often 2–10×)
- Are for testing, not production traffic
Shadow memory tracks happens-before; if it reports a race, trust it.
cgo Boundary (Preview)
cgo inserts C ABI calls and can:
- Prevent easy cross-compilation
- Block some optimizations
- Require
CGO_ENABLED=1and a toolchain
Deep systems: cgo. Prefer pure Go drivers when shipping multi-arch binaries.
Reproducible Builds
go build -trimpath -ldflags='-buildid=' -o app .Trim paths remove local directory noise from binaries; useful for supply-chain hygiene.
Experiment
go mod init example
go build -o /tmp/app-normal .
go build -ldflags='-s -w' -o /tmp/app-strip .
ls -la /tmp/app-normal /tmp/app-strip
go test -race -c -o /tmp/app.test .
# file sizes differ; race binary is larger
ls -la /tmp/app.testpackage main
import "fmt"
var version = "dev"
func main() {
fmt.Println("version", version)
}go build -ldflags='-X main.version=1.0.0' -o /tmp/app-ver .
/tmp/app-verWhat to notice: Strip and race change binary size dramatically; -X injects version at link time.
Try next: Build with CGO_ENABLED=0 and confirm ldd (Linux) shows no unexpected libc deps for a pure Go net/http binary.