Open this lesson in your favourite AI. It'll walk you through the why, explain the demo, and quiz you on the try-it list.
Go's killer ops feature: a static binary, no runtime dependencies, that you can cross-compile from a Mac to a Linux server in one command. No Docker required, no Rust-style toolchain installs. This is why Go dominates ops tooling (Kubernetes, Terraform, Docker itself). Knowing the matrix of GOOS × GOARCH, when CGo breaks cross-compilation, and how -ldflags strips your binary down to a few MB is the daily work of shipping Go.
Go compiles to a native binary for the target platform — no JVM, no interpreter, no runtime to install at the destination. Two environment variables control the target: GOOS (linux, darwin, windows) and GOARCH (amd64, arm64). This means your Mac dev machine can produce a Linux binary to deploy in a container, or an ARM binary for a Raspberry Pi, without a separate cross-compiler toolchain. The -ldflags flag lets you inject values at link time — a version string or commit hash — so your production binary can report exactly which code it contains. This is the foundation of how Go programs ship as standalone binaries: no dependency installation step at the target.
file <binary> to verify each.-ldflags='-s -w' and compare sizes — typically ~30% smaller. Verify it still runs.-X main.version=1.2.3. Print it from main and confirm it changed.go tool dist list. Note the exotic targets (riscv64, ppc64) — Go runs on more places than you'd think.Use these three in order. Each builds on the one before.
In one paragraph, explain why Go can cross-compile so easily when C/C++ can't — what's different about Go's runtime?
Walk me through what `-ldflags='-s -w'` actually does (DWARF, symbol tables) and why stripping is safe for production binaries but breaks debugging.
When does CGo break cross-compilation, and what's the workaround? Cover `CGO_ENABLED=0`, alpine vs glibc, and `musl` for static linux binaries.
// main.go — same source, every platform.
package main
import (
"fmt"
"runtime"
)
// version is overridden at build time via -ldflags="-X main.version=...".
// The default "dev" only appears in unstamped local builds.
var version = "dev"
func main() {
// runtime.GOOS / runtime.GOARCH are constants the compiler bakes in.
// They reflect the *target* of the build, not the host that ran 'go build'.
fmt.Printf("hi from %s/%s, version %s\n", runtime.GOOS, runtime.GOARCH, version)
}
/* ---------- now build it from one Mac, for four targets ----------
# 1) default — host platform (e.g. darwin/arm64 on an M-series Mac):
$ go build -o hi main.go
$ ./hi
hi from darwin/arm64, version dev
# 2) Linux x86_64 (most cloud VMs, GitHub Actions runners):
$ GOOS=linux GOARCH=amd64 go build -o hi-linux-amd64 main.go
$ file hi-linux-amd64
hi-linux-amd64: ELF 64-bit LSB executable, x86-64, statically linked
# 3) Linux ARM64 (AWS Graviton, Raspberry Pi 4+, Apple Silicon containers):
$ GOOS=linux GOARCH=arm64 go build -o hi-linux-arm64 main.go
# 4) Windows x86_64:
$ GOOS=windows GOARCH=amd64 go build -o hi.exe main.go
# strip DWARF + symbol table to shrink the binary by ~30%:
$ go build -ldflags="-s -w" -o hi-small main.go
# inject the version at link time — overwrites the 'dev' default:
$ go build -ldflags="-X main.version=1.2.3" -o hi main.go
$ ./hi
hi from darwin/arm64, version 1.2.3
# combine: stripped, versioned, Linux:
$ GOOS=linux GOARCH=amd64 go build \
-ldflags="-s -w -X main.version=1.2.3" \
-o hi-linux-amd64 main.go
# enumerate every target the toolchain supports:
$ go tool dist list | head -10
aix/ppc64
android/386
android/amd64
android/arm
android/arm64
darwin/amd64
darwin/arm64
dragonfly/amd64
freebsd/386
freebsd/amd64
---------- */go run main.go