Skip to content

Day 052 — Build Tags & Cross-Compilation

Month 2 · Week 4 · ⬅ Day 051 · Day 053 ➡ · Journal index

🎯 Learning Objective

Compile platform- and feature-specific variants of the same package using //go:build constraints and filename suffixes, and cross-compile a binary for another OS/arch with GOOS/GOARCH.

📚 Topics

  • //go:build constraint expressions (&&, ||, !, parens)
  • Implicit filename-suffix constraints (_windows.go, _amd64.go, _test.go)
  • Custom tags via -tags; cross-compilation with GOOS/GOARCH

📖 Reading / Sources

📝 Notes

  • A build constraint decides whether a file is compiled at all. Modern form is a //go:build line; it must sit at the top of the file, followed by a blank line before the package clause. The old // +build form is deprecated (kept only for pre-1.17 toolchains). → links to [[tooling]].
  • The constraint is a boolean expression over tags: //go:build (linux || darwin) && !cgo. Tags include every GOOS, every GOARCH, cgo, the Go version (go1.22), and any custom identifier you pass with -tags foo.
  • Filename suffixes are implicit constraints: a file ending _windows.go compiles only for GOOS=windows; _amd64.go only for GOARCH=amd64; _windows_amd64.go for both. _test.go is the suffix that makes a file test-only. You don't need a //go:build line for the suffix to apply, but adding one lets you combine the OS with extra tags.
  • Per-platform files providing the same symbol must be mutually exclusive and exhaustive, or you get "X redeclared in this block" (overlap) or "undefined: X" (a gap). Include a catch-all like //go:build !linux && !darwin && !windows.
  • Build tags resolve the variant at compile time — there is no if runtime.GOOS == ... branch and no dead code for other platforms in the binary. Contrast with runtime.GOOS, which is a runtime value.
  • Cross-compilation is built in: set GOOS/GOARCH and go build emits a binary for that target without a C toolchain as long as CGO_ENABLED=0 (the default when cross-compiling). GOOS=windows GOARCH=amd64 go build produces a .exe from a Linux box.
  • runtime.GOOS/runtime.GOARCH report the target baked in at build time, so a cross-built binary prints its target, not the build host. List all targets with go tool dist list.

💻 Code Examples

//go:build linux || darwin

// greeting_unix.go — compiled ONLY for the unix-like targets. A sibling
// greeting_windows.go and a catch-all greeting_other.go define the same
// platformGreeting() for the other targets; exactly one is linked in.
package main

func platformGreeting() string { return "Hello from a Unix-like build" }

Full example: examples/month-02/build-tags/ · Run host build: go run ./examples/month-02/build-tags · Feature tag: go run -tags debug ./examples/month-02/build-tags · Cross-compile: GOOS=windows GOARCH=amd64 go build -o /tmp/bt.exe ./examples/month-02/build-tags

🏋️ Exercises / Practice

Exercise Status Link
Add a debug_on.go/debug_off.go pair gated on -tags debug examples/month-02/build-tags
Cross-compile the URL shortener for windows/amd64 and darwin/arm64 examples/month-02/urlshortener

🐛 Mistakes Made

  • Forgot the blank line between //go:build linux and package main → the line was parsed as an ordinary comment and the constraint silently did nothing.
  • Made the per-OS constraints overlap (linux in two files) → platformGreeting redeclared. Fixed by making them mutually exclusive.

❓ Open Questions

  • When do I actually need cgo (so cross-compilation needs a cross C toolchain) vs pure-Go? (Pure Go cross-compiles freely; cgo pulls in a C compiler per target — avoid unless a dependency forces it.)

🧠 Active Recall (answer without looking)

  1. Q: What two formatting rules make a //go:build line actually take effect?
    A

It must be near the top of the file before the package clause, and it must be followed by a blank line separating it from package. Otherwise it's just a comment. 2. Q: Does runtime.GOOS tell you the build host or the build target?

A

The target — it's the GOOS the binary was compiled for, fixed at build time. A binary cross-built with GOOS=windows reports windows even if built on Linux.

🪶 Feynman Reflection

Build tags are an on/off switch on each source file, evaluated before compilation. The switch reads the target OS/arch and any -tags you pass, so you can keep windows-only and linux-only code in separate files and let the toolchain pick the right one. Because the choice happens at compile time, cross-compiling is just changing two environment variables — no other-platform code rides along in the binary.

🕳️ Knowledge Gaps

  • embed + build tags for asset variants; and purego patterns to dodge cgo — note for later.

✅ Summary

I can gate files with //go:build expressions and filename suffixes, keep per-platform symbols mutually exclusive, pass custom -tags, and cross-compile with GOOS/GOARCH.

⏭️ Next Steps / Prep for Tomorrow

  • Day 053: profile a running program with runtime/pprof and net/http/pprof, then read the profile with go tool pprof.

Time spent Difficulty Confidence
90 min 🟦🟦⬜⬜⬜ 🟦🟦🟦⬜⬜

Suggested commit: docs(journal): build tags and cross-compilation (day 052)