Cross-compiling Go programs
You wrote a CLI tool on your Mac. It works great. Now you need to run it on a headless Linux server in a data center, or maybe a Raspberry Pi controlling a smart home device. You don't have a Linux machine handy. You don't want to spin up a VM just to compile. Go makes this trivial. You can build binaries for almost any operating system and CPU architecture from a single machine. The compiler handles the translation internally. You just tell it where the binary is going.
How the target works
Go separates the compilation target from the build machine. The compiler doesn't care what CPU is running the build process. It only cares about two environment variables: GOOS for the operating system and GOARCH for the architecture. Set these, and the Go toolchain generates machine code for the target instead of the host.
This works because Go's standard library is written mostly in Go. When you import os or net, the compiler includes Go code that adapts to the target platform. There is no hidden C dependency dragging in a host-specific toolchain. The compiler emits the correct instructions and packaging format for the target. For Linux, it produces an ELF executable. For Windows, it produces a PE file. For macOS, it produces a Mach-O binary.
Go uses darwin to refer to macOS. This matches the kernel name. If you try GOOS=macos, the compiler rejects it with unsupported GOOS/macos. Always use darwin. Similarly, GOARCH values are lowercase. Use amd64, not x86_64. Use arm64, not aarch64. The toolchain normalizes these names.
The compiler does the heavy lifting. You just point the target.
Minimal example
Here's the simplest way to cross-compile. Set the variables and run go build.
# Target Linux on a 64-bit Intel/AMD CPU
GOOS=linux GOARCH=amd64 go build -o myapp-linux-amd64 ./cmd/myapp
The GOOS=linux flag tells the compiler to generate ELF binaries for Linux. The GOARCH=amd64 flag targets 64-bit x86 processors. The -o flag sets the output filename so you don't overwrite your local binary. The ./cmd/myapp argument specifies the package path to compile. The result is a standalone binary that runs on Linux without needing Go installed there.
Walkthrough
When you run this command, the Go compiler reads GOOS and GOARCH. It switches its code generator to emit instructions for the target CPU and packaging format for the target OS. The standard library is compiled for the target as well. If your code uses os/exec or syscall, the compiler includes the correct stubs for the target platform.
The build process links everything into a single file. Go binaries are statically linked by default. They don't depend on shared libraries on the target machine. You can copy the binary to any machine with the matching OS and architecture, and it runs. This is a major advantage over languages that rely on dynamic linking. You don't need to worry about missing glibc versions or DLL hell on the deployment target.
Realistic example
Real projects often need binaries for several platforms at once. A shell loop handles this efficiently. This pattern is common in release scripts.
# Build for common OS/arch combinations
for os in linux windows darwin; do
# darwin is macOS, linux is Linux, windows is Windows
for arch in amd64 arm64; do
# GOOS and GOARCH are exported for this command only
GOOS=$os GOARCH=$arch go build -o myapp-$os-$arch ./cmd/myapp
done
done
The outer loop iterates over target operating systems. The inner loop iterates over CPU architectures. The variables are scoped to the go build command, so they don't affect the rest of the shell session. The output filename includes the OS and arch for easy identification. This produces four binaries: myapp-linux-amd64, myapp-linux-arm64, myapp-windows-amd64, myapp-windows-arm64, myapp-darwin-amd64, and myapp-darwin-arm64.
Special targets
Go supports more than desktop and server platforms. You can compile to WebAssembly and Android.
WebAssembly allows Go code to run in browsers. Set GOOS=js and GOARCH=wasm. This produces a .wasm file, not a binary. You need a JavaScript loader to run it in a browser. This is useful for embedding Go logic in web apps.
# Compile Go to WebAssembly for browser execution
GOOS=js GOARCH=wasm go build -o myapp.wasm ./cmd/myapp
The GOOS=js flag targets the JavaScript environment. The GOARCH=wasm flag generates WebAssembly bytecode. The output is a .wasm file, not an executable binary. You must serve this file alongside a JavaScript bootstrap script provided by the Go standard library.
Android support lets you build Go code for mobile devices. Set GOOS=android and GOARCH=arm64. This produces an APK-compatible binary. You still need the Android SDK to package it into an installable app, but the Go compilation step is handled by the toolchain.
# Build for Android on 64-bit ARM devices
GOOS=android GOARCH=arm64 go build -o myapp-android ./cmd/myapp
The GOOS=android flag targets the Android OS. The GOARCH=arm64 flag targets 64-bit ARM processors common in modern phones. The binary follows the Android NDK naming conventions. You can link this binary into an Android app using the standard build tools.
Pitfalls and errors
Cross-compilation works perfectly for pure Go code. Problems arise when you use cgo. The cgo tool lets Go call C code. If your project imports a package that uses C, the compiler needs a C compiler for the target architecture. On a Mac, the default C compiler targets macOS. It cannot produce Linux binaries. You get an error like exec: "gcc": executable file not found in $PATH or a linker error about incompatible architectures.
Disable cgo to build pure Go binaries for any target. This works if your dependencies don't require C libraries. The resulting binary is fully static and portable.
# Disable CGO to build pure Go binaries for any target
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o myapp-linux ./cmd/myapp
The CGO_ENABLED=0 flag forces the compiler to skip C code. This works if your dependencies don't require C libraries. The resulting binary is fully static and portable. If you need cgo, you must install a cross-compiler toolchain for the target OS. This is complex and error-prone. Using a Docker container that matches the target OS is often easier.
ARM processors have many variants. Raspberry Pi models often use 32-bit ARM. The GOARCH=arm flag isn't enough. You must set GOARM to specify the instruction set version. A Raspberry Pi 3 or 4 usually needs GOARM=7. If you omit this, the binary might run on a newer device but crash on an older one with an illegal instruction error.
# Target 32-bit ARM with ARMv7 instructions for Raspberry Pi
GOOS=linux GOARCH=arm GOARM=7 go build -o myapp-pi ./cmd/myapp
The GOARCH=arm flag selects 32-bit ARM architecture. The GOARM=7 flag enables ARMv7 instruction set. This is required for Raspberry Pi 3 and 4 models. GOARM is only valid when GOARCH=arm. It has no effect on arm64.
For x86_64, you can tune performance with GOAMD64. Setting GOAMD64=v3 enables AVX2 instructions. This makes math-heavy code faster on modern CPUs but breaks on older Intel processors. Use this when you control the deployment environment and know the CPU generation.
# Optimize for modern Intel/AMD CPUs with AVX2 support
GOOS=linux GOARCH=amd64 GOAMD64=v3 go build -o myapp-fast ./cmd/myapp
The GOAMD64=v3 flag enables AVX2 and BMI instructions. This improves performance for cryptographic and math operations. The binary will crash on CPUs older than Skylake (2015). Check the CPU generation before using this flag.
Not all combinations work. You can't build for GOOS=js with GOARCH=amd64. The compiler checks compatibility. If you pick a bad pair, you get unsupported GOOS/GOARCH pair. The full list of supported pairs is in the Go source code, but the common combinations are well-documented.
Always test cross-compiled binaries. A container is the fastest way. Run a lightweight Linux image to execute the binary without installing Linux.
# Test a Linux binary using Docker without installing Linux
docker run --rm -v "$(pwd):/app" -w /app alpine ./myapp-linux
The --rm flag removes the container after execution. The -v flag mounts the current directory into the container. The -w flag sets the working directory to the mounted volume. The alpine image is a lightweight Linux distribution for quick testing. This verifies the binary runs correctly on the target OS.
CGO breaks portability. Disable it or containerize the build.
ARM is not one architecture. Check the chip before you build.
Decision matrix
Use GOOS and GOARCH flags for one-off builds when you need a binary for a different platform quickly. Use a shell loop or Makefile when you distribute releases and need binaries for multiple platforms simultaneously. Use CGO_ENABLED=0 when your project depends on C code and you lack a cross-compiler toolchain for the target OS. Use GOARM when targeting 32-bit ARM devices like older Raspberry Pi models to ensure the binary supports the correct instruction set. Use GOAMD64 when you control the deployment environment and want to optimize performance for modern CPUs. Use a Docker container matching the target OS when you must use cgo and need a reliable C toolchain without configuring cross-compilers manually. Use go build with no environment variables when building for the current machine; the toolchain defaults to the host OS and architecture automatically.
Cross-compilation is a feature, not a hack. Use it to ship everywhere.