The binary that won't run
You spend three nights building a CLI tool that parses log files. It works perfectly on your laptop. You zip it up and send it to a teammate who runs Windows. They reply with a screenshot: the file won't open, or the terminal spits out an error. You didn't write bad code. You built the wrong binary.
Go makes this fix take ten seconds, but you need to tell the compiler where the binary is going before it starts generating machine code. Cross-compilation in Go is not a complex toolchain setup. It is a configuration change. You set two environment variables, and the compiler emits a binary for a different operating system and CPU architecture than your development machine.
Two dials, one compiler
Cross-compilation means building a program on one machine to run on another. Go treats this as a configuration problem. The compiler ships with the knowledge to emit binaries for dozens of platforms. You don't need to install a Windows compiler on Linux or a macOS linker on a Raspberry Pi.
The toolchain has a switchboard with two main dials: GOOS for the operating system and GOARCH for the CPU architecture. You set these variables in the environment, and the compiler switches its backend. It generates assembly instructions for the target CPU and wraps the result in the correct file format for the target OS.
Linux uses ELF files. Windows uses PE files. macOS uses Mach-O files. The compiler handles the headers, the entry points, and the symbol tables for the target. The result is a file that the target OS loader can execute directly.
The command
Here is the simplest cross-compilation command: set the variables, run build, and name the output file.
# GOOS selects the target operating system
# GOARCH selects the CPU architecture
# The compiler generates a Windows PE binary for 64-bit Intel/AMD
GOOS=windows GOARCH=amd64 go build -o myapp.exe .
The -o flag names the output file. The convention is to include the OS and arch in the filename so you don't mix up binaries. The . at the end tells go build to compile the current package.
What happens under the hood
When you run go build, the toolchain checks the environment. If GOOS and GOARCH are set, the compiler validates the pair. Not every OS supports every architecture. The compiler rejects invalid combinations with unknown GOOS darwinx or unsupported GOOS/GOARCH darwin/386.
Once validated, the compiler selects the backend. It generates instructions for the target CPU instead of the host CPU. If you are on an Intel Mac and set GOARCH=arm64, the compiler emits ARM instructions. It also changes the binary format. The linker bundles the Go runtime, your code, and any pure Go dependencies into a single file.
The build process stays fast. The compiler doesn't need to run the target OS. It just needs the tables that describe the instruction set and the binary format. These tables are built into the Go distribution.
Picking the right target
The source of truth for supported targets is go tool dist list. This command prints every valid GOOS/GOARCH pair your Go version supports.
# go tool dist list prints all supported OS and architecture combinations
# Use this to verify targets before building, especially for embedded or legacy systems
go tool dist list
Common targets include linux/amd64, darwin/arm64, windows/amd64, and linux/arm64. You will also see js/wasm, which compiles Go to WebAssembly for the browser. The Go community treats go tool dist list as the reference. Don't guess the spelling. If it's not in the list, the compiler won't accept it.
Some targets have quirks. GOOS=android produces binaries for Android devices. GOOS=ios requires a macOS host and the Xcode toolchain because Apple restricts code signing. For most standard targets, GOOS and GOARCH are all you need.
The release build
Real-world builds usually require more than just the OS and arch. The standard pattern for production binaries includes disabling CGO.
# CGO_ENABLED=0 forces pure Go compilation
# This creates a static binary with no external C dependencies
# GOOS=linux targets the server environment
# GOARCH=arm64 matches modern cloud instances and Raspberry Pi 4
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o myapp-linux-arm64 .
CGO allows Go code to call C libraries. When CGO is enabled, the compiler invokes a C compiler to build those bindings. By default, it uses the C compiler on the host machine. If you are cross-compiling, the host C compiler produces code for the host, not the target. This breaks the build or produces a binary that crashes at runtime.
Setting CGO_ENABLED=0 tells Go to skip C compilation entirely. The linker bundles everything into a single static binary. The binary runs on the target without needing external shared libraries. This eliminates the "it works on my machine" problem caused by missing libc versions or glibc mismatches. The Go community treats CGO_ENABLED=0 as the default for production builds. It makes deployment simpler and more reliable.
Pitfalls and errors
The most common trap is forgetting CGO. If your code imports C or a package that uses C bindings, the compiler tries to invoke a C compiler. If you don't have one installed, the build fails with exec: "gcc": executable file not found in $PATH. If you do have one, the compiler links against the host C libraries. The resulting binary might run on the host but fail on the target with error while loading shared libraries: libc.so.6: cannot open shared object file.
The fix is CGO_ENABLED=0. If you strictly need C bindings, you must install a cross-compiler toolchain and set CC and CXX to point to it. Most Go projects don't need C. Disabling CGO is the standard move.
Typos in GOOS or GOARCH are another frequent error. GOOS=darwinx go build results in unknown GOOS darwinx. The compiler validates the variables early. It won't silently accept a wrong value.
Runtime errors happen when you run the wrong binary. On Linux, trying to execute an ARM binary on an Intel machine produces cannot execute binary file: Exec format error. The OS loader checks the binary header and rejects it if the architecture doesn't match. Always verify the binary with file myapp or go version -m myapp before deploying.
Cross-compilation is a configuration, not a ceremony. Set the variables, disable CGO, and test on the target.
Tuning for the CPU
Beyond the OS and architecture, you can tune the compiler for specific CPU generations. This is useful when you know the target hardware and want to squeeze out extra performance.
The GOAMD64 variable controls instruction set features for 64-bit Intel and AMD CPUs. The default is v1, which targets the baseline instruction set supported by all 64-bit chips. Setting GOAMD64=v3 enables AVX2 instructions. This can speed up math, string processing, and crypto operations significantly. The trade-off is that the binary crashes on older CPUs that don't support AVX2.
# GOAMD64=v3 enables AVX2 instructions for faster math and crypto
# Only use this if you know the target CPU supports v3 features
# v1 is the safe default that runs on all 64-bit Intel/AMD chips
GOAMD64=v3 go build -o myapp-fast .
For ARM devices, the GOARM variable selects the ARM version. This applies only to 32-bit arm targets, not arm64. GOARM=6 targets older chips. GOARM=7 targets ARMv7 CPUs with hardware floating point, which is the correct setting for Raspberry Pi 3 and 4.
# GOARM=7 targets ARMv7 CPUs with hardware floating point
# This is the correct setting for Raspberry Pi 3 and 4
# Omit GOARM for arm64 targets; it only applies to 32-bit arm
GOOS=linux GOARCH=arm GOARM=7 go build -o myapp-pi .
Tune the CPU, not just the OS. If you know the hardware, use GOAMD64 or GOARM to generate optimized code. If you need maximum compatibility, stick to the defaults.
When to use what
Use GOOS and GOARCH when you need a binary for a different platform than your development machine. Use CGO_ENABLED=0 when you want a single static binary that runs without external dependencies. Use go tool dist list when you are unsure which GOOS or GOARCH values your Go version supports. Reach for a cross-compiler toolchain when your code strictly requires C bindings and you cannot disable CGO. Stick to a native build when you are developing locally and just need to test logic quickly. Use GOAMD64=v3 when you are deploying to modern Intel/AMD servers and want to optimize performance for AVX2. Use GOARM=7 when targeting Raspberry Pi 3 and 4 with 32-bit Go.
CGO is the leak in the boat. Plug it with CGO_ENABLED=0.