Linking, Build Modes, and Race ABI

Updated

September 8, 2026

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/app

Static-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.

Build Tags and Constraints

//go:build linux && amd64

package onlylinux
go build -tags=debug,integration

Use tags for optional integration tests and platform files (foo_windows.go), not for sprawling feature flags that should be config.

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=1 and 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.test
package 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-ver

What 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.