How do you cross-compile a Go binary for Linux arm64 from a macOS laptop?
answer
- two variables, one build command
- the target OS and the target CPU
- no cross-toolchain to install
- GOOS and GOARCH, set per invocation
- go tool dist list shows the legal pairs
basics
~20 sSet GOOS and GOARCH for that one build: GOOS=linux GOARCH=arm64 go build ./cmd/tool. The Go toolchain compiles for every target it supports on its own, so there is no separate cross-compiler, VM or target SDK to install.
solid answer
~50 sYou set two environment variables on the build command: `GOOS` names the target operating system and `GOARCH` the target CPU architecture, so `GOOS=linux GOARCH=arm64 go build ./cmd/tool` on a macOS machine produces a Linux arm64 executable. Nothing else is installed — the Go compiler and linker are target-parameterised and the standard library is compiled from source for whichever pair you ask for, which is why cross-building is a first-class, everyday operation in Go rather than a special setup. `go tool dist list` prints every GOOS/GOARCH pair the toolchain accepts; an unsupported pair is rejected with an `unsupported GOOS/GOARCH pair` error. Two consequences to expect: with `GOOS=windows` the go command appends `.exe` to the output name, and the artifact you just produced cannot be executed on the build machine — you need the target hardware, an emulator or a container to run it.
code
text · 7 linesGOOS=linux GOARCH=arm64 go build ./cmd/promote
GOOS=windows GOARCH=amd64 go build ./cmd/promote # writes promote.exe
go tool dist list | head -3
# aix/ppc64
# android/386
# android/amd64go deeper
Be ready to say the two variable names without hesitating and to write the one-line command that builds for another platform. Also know that the result cannot be run on your own machine.
Explain why no extra toolchain is required: the compiler targets every port and the standard library is compiled from source per target and cached. Mention go tool dist list as the source of truth for legal pairs.
Show the operational habits: variables set per invocation rather than exported, the pair encoded in artifact names, and a real execution on the target before release because a green cross-build only proves compile and link.
Frame it as a distribution decision — which platforms you promise to support, what that costs in CI runners and emulators, and where the line sits between shipping a prebuilt binary and telling users to build from source.
## The two variables Every Go build has a target described by two values: - **`GOOS`** — the target operating system: `linux`, `darwin` (macOS), `windows`, `freebsd`, `android`, and others. - **`GOARCH`** — the target CPU architecture: `amd64` (64-bit x86), `arm64`, `arm` (32-bit ARM), `386`, `ppc64le`, `riscv64`, and others. When you do not set them, they default to the machine you are building on. Setting them for a single invocation is all that cross-compilation requires: ``` GOOS=linux GOARCH=arm64 go build ./cmd/tool ``` That form sets the variables only for that command, which is what you want — leaving `GOOS` exported in your shell, or persisting it with `go env -w GOOS=linux`, silently retargets everything afterwards, including `go test`, which then compiles fine and fails to execute the test binary. ## Why no cross-toolchain is needed In most ecosystems, targeting another platform means installing a cross-compiler and a target-specific copy of the runtime and system libraries. Go avoids that: the compiler and linker shipped with your toolchain can emit code for every port, and the standard library is distributed as source and compiled for the requested pair on demand (the results are kept in the build cache, so the first cross-build of a target is slower than the next one). A pure-Go program therefore has nothing platform-specific to fetch. This is a deliberate property of the toolchain and the main reason Go became a common choice for shipping command-line tools as prebuilt binaries for many platforms. ## Discovering valid pairs Not every combination exists. `go tool dist list` prints the supported pairs, one `GOOS/GOARCH` per line: ``` darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 windows/amd64 ``` `go tool dist list -json` prints the same set with per-port detail, including whether the port is a *first-class* one — the small group the Go project tests on every release and publishes official downloads for. If you pass a pair that does not exist, the go command stops with an `unsupported GOOS/GOARCH pair` message rather than producing something broken. ## What you get, and what you do not The output is a native executable for the target: an ELF binary for Linux, a Mach-O binary for macOS, a PE binary for Windows. Two practical points follow. First, **naming**: with `GOOS=windows` the go command appends `.exe` automatically, so the same build command produces `tool` for Linux and `tool.exe` for Windows. Release pipelines usually also encode the pair in the filename (`tool_linux_arm64`) so users can pick the right download. Second, **you cannot run it locally**. A Linux arm64 binary on a macOS laptop is just a file; attempting to execute a foreign-architecture binary on Linux gives `cannot execute binary file: Exec format error`. A successful cross-build proves the package compiles and links for that target — it proves nothing about behaviour. Getting real coverage means running the artifact on the target architecture, in an emulator, or on a CI runner of that platform. ## The usual first surprise Code that builds happily for your own machine can fail to build for another one, because the go tool selects source files per platform: a file whose name ends in `_linux.go` is compiled only when `GOOS=linux`. Retarget the build and that file vanishes from the package, so a symbol it declared becomes `undefined`. That is not a bug in cross-compilation; it is the same file-selection mechanism that lets one repository hold both the Linux and the Windows implementation of a feature. ## Rule of thumb Treat `GOOS`/`GOARCH` as per-invocation build inputs, discover legal values with `go tool dist list`, keep them out of your persistent environment, and remember that building for a platform and testing on it are two different activities.
- How do you find out which GOOS/GOARCH pairs the toolchain will accept?`go tool dist list` prints them one per line, and `go tool dist list -json` adds per-port detail such as whether the port is first class. Passing a pair that is not on the list fails immediately with an `unsupported GOOS/GOARCH pair` error, so a typo like `GOARCH=arm86` never produces a silently wrong artifact.
- Does setting GOOS=windows change anything about the output file besides its contents?Yes — the go command appends `.exe` to the executable name when the target is Windows, so the same build command yields `tool` for Linux and `tool.exe` for Windows. Release scripts normally add the pair to the name as well, producing something like `tool_windows_amd64.exe`, so users can identify their download.
- Why does Go not need a separate cross-compiler for each target?The compiler and linker in your toolchain can emit code for every supported port, and the standard library ships as source that is compiled for whichever pair you request and then cached. So retargeting is a matter of two environment variables rather than installing a target SDK, sysroot or second toolchain.
- What goes wrong if you set GOOS permanently with go env -w?Every later go command inherits it. Builds still succeed, but `go test` produces a test binary for the wrong platform and fails to execute it, and `go run` cannot start the program. Set the variables inline on the one command that needs them and leave your default environment pointing at your own machine.
saying these in an interview costs you the question
- Claims you must install a cross-compiler or target SDK first
- Thinks a Linux binary requires building inside a Linux VM or container
- Believes the arm64 artifact can be executed on the amd64 build machine
- Sets GOOS with go env -w and then wonders why go test fails
- Confuses GOARCH with GOARM or with the microarchitecture level