Building Executables — Find the Bug¶
Each scenario shows a command or setup that looks fine but misbehaves. Find the defect, explain it, and fix it.
Bug 1 — Stripped binary, but no pprof source lines¶
go build -ldflags="-s -w" -o api ./cmd/api
# later, in production:
curl http://prod/debug/pprof/profile?seconds=30 > prof.out
pprof -http=:8080 prof.out
# flame graph shows function names but no source lines, no inlining detail
Bug: -w stripped DWARF, and -s stripped the symbol table. pprof falls back to .gopclntab, which gives function names but not full source-line/inlining info. Fix: keep an unstripped build artifact (same build ID) and symbolize the profile against it offline: pprof -http=:8080 ./api.unstripped ./prof.out. Archive unstripped binaries by build ID for every release.
Bug 2 — Developer home directory in panic traces¶
panic: runtime error: index out of range [3] with length 2
goroutine 1 [running]:
main.handle(...)
/Users/alice/projects/secret-startup/internal/server/handler.go:42
Bug: the build did not pass -trimpath, so absolute file system paths from the build machine were embedded in stack traces and BuildInfo. Fix: always build releases with -trimpath. Stack traces then show paths like secret-startup/internal/server/handler.go:42, module-relative and free of build-host details.
Bug 3 — Version variable not overridden¶
go build -ldflags="-X github.com/me/proj/version.Version=v1.2.3" -o api ./cmd/api
./api
# prints "dev"
Bug: Version is a const. -X can only set package-level string variables, not constants. The linker silently does nothing for the wrong kind of symbol. Fix: make it a var:
Bug 4 — runtime/debug.BuildInfo is empty¶
Bug: the binary was not built via go build from a module — for example, it was built by a custom tool that invoked cmd/compile and cmd/link directly, or via go build in GOPATH mode, or it was a test binary in an odd configuration. The BuildInfo section is only emitted by the standard module-aware go build. Fix: build via go build ./cmd/api inside the module. For diagnostic builds, also confirm with go version -m ./api that the build info section is present; if it is not, your build path skipped the standard linker invocation.
Bug 5 — Container image fails on Alpine because of dynamic libc¶
FROM golang:1.23 AS build
COPY . .
RUN go build -o /out/api ./cmd/api # CGO_ENABLED defaults to 1 with gcc present
FROM alpine:3.20
COPY --from=build /out/api /api
ENTRYPOINT ["/api"]
Bug: the build image has gcc, so CGO_ENABLED defaulted to 1 and produced a dynamically linked binary against glibc. Alpine has musl, not glibc, so the dynamic linker cannot resolve the binary and reports the misleading "no such file or directory." Fix: force a pure-Go build with CGO_ENABLED=0 go build .... Better, move to gcr.io/distroless/static-debian12:nonroot as the final image so the static binary lives in an environment that matches it.
Bug 6 — Cross-compile overwrites previous output¶
for target in linux/amd64 linux/arm64 darwin/arm64; do
GOOS=${target%/*} GOARCH=${target#*/} go build -o bin/api ./cmd/api
done
ls bin/
# bin/api ← only the last build survives
Bug: the output name bin/api is identical for every iteration; each cross-build overwrites the previous one. The final file is whichever target ran last (darwin/arm64), which is useless on Linux. Fix: include ${GOOS} and ${GOARCH} (and version) in the output name:
out="bin/api-${VERSION}-${GOOS}-${GOARCH}"
[ "$GOOS" = "windows" ] && out="${out}.exe"
go build -o "$out" ./cmd/api
Bug 7 — CI silently bumped go.mod¶
A teammate notices that CI builds use a newer transitive dependency than local builds, and go.sum keeps getting updated by CI.
Bug: without -mod=readonly (or GOFLAGS=-mod=readonly), go build is allowed to update go.mod/go.sum when it discovers a new requirement. CI mutating dependencies between builds breaks reproducibility and hides regressions. Fix: add -mod=readonly to every CI build (or set GOFLAGS=-mod=readonly in the job env). The build now fails loudly if dependencies would change, forcing a deliberate go mod tidy PR.
Bug 8 — Container ships the compiler image¶
The resulting image is ~900 MB; the security team flags ~70 CVEs from the toolchain layer.
Bug: the final image is golang:1.23, which contains the full Go toolchain, GCC, build deps, and the entire source tree. Production has no need for any of that. Fix: use a multi-stage build whose final stage is a minimal base:
FROM golang:1.23 AS build
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/api ./cmd/api
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/api /api
USER nonroot:nonroot
ENTRYPOINT ["/api"]
Bug 9 — Reproducible build differs on two machines¶
# machine A
go build -trimpath -ldflags="-s -w -X main.version=v1.0.0" -o api ./cmd/api
sha256sum api
# 3a7b...
# machine B, same commit, same Go version, same flags
go build -trimpath -ldflags="-s -w -X main.version=v1.0.0" -o api ./cmd/api
sha256sum api
# 9c14...
Bug: the link-time build ID differs per machine (it derives from inputs including paths and environment), so the binaries are not byte-identical even with -trimpath. Fix: clear the build ID explicitly to enable bit-reproducibility:
go.sum also match. Bug 10 — macOS binary blocked by Gatekeeper on first launch¶
A customer downloads api-v1.2.3-darwin-arm64.tar.gz from your release page, extracts it, runs ./api, and gets:
Bug: the binary was signed (or not) but was never notarized through Apple. Gatekeeper blocks unverified binaries on first launch. Fix: sign with a Developer ID certificate, submit to Apple for notarization, and staple the ticket:
codesign --sign "Developer ID Application: Acme (TEAMID)" --options runtime --timestamp api
ditto -c -k --keepParent api api.zip
xcrun notarytool submit api.zip --apple-id ... --team-id TEAMID --password ... --wait
xcrun stapler staple api
How to approach these¶
- Production stack trace looks wrong? → check
-trimpathand that-s -wis not blocking the use case you need. -Xsilently no-op? → check that the target is avar(notconst) and the full import path is correct.BuildInfoempty? → check the binary was built via the standardgo buildfrom a module.- Binary fails on Alpine/distroless? → set
CGO_ENABLED=0and pick a base that matches your linkage. - Cross-compile overwriting itself? → include
${GOOS}-${GOARCH}in-o. - CI drifting? → enforce
-mod=readonly. - Image too large? → multi-stage build ending in distroless or scratch.
- Reproducibility broken? → strip the build ID and pin everything (toolchain, sums, flags, time).
- macOS blocked? → notarize and staple, do not just sign.