skip to content

Shipping Binaries

Turning packages into an artifact somebody else can run: which GOOS and GOARCH it targets, whether cgo left it dynamically linked, and what version it reports about itself.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

explore

questions

13

How do you cross-compile a Go binary for Linux arm64 from a macOS laptop?

level: juniorimportance: must knowfreq 62%

answer

  1. two variables, one build command
  2. the target OS and the target CPU
  3. no cross-toolchain to install
  4. GOOS and GOARCH, set per invocation
  5. go tool dist list shows the legal pairs

basics

~20 s

Set 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 s

You 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 lines
text
GOOS=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/amd64

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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
open as a page

What does go build -ldflags "-X main.version=1.4.0" do to a Go binary?

level: juniorimportance: must knowfreq 72%

basics

~20 s

It tells the Go linker to set the package-level string variable named version in package main to 1.4.0 when the binary is linked, so the compiled program can report that version without it being hard-coded in the source.

open as a page

What does building with CGO_ENABLED=0 change about a Go binary, and what do you give up?

level: juniorimportance: must knowfreq 70%

basics

~20 s

CGO_ENABLED=0 disables cgo, so no C code is compiled in and the standard library uses its pure-Go net and os/user code. The result is a self-contained static binary that needs no C library on the host.

open as a page

A Go binary built on a glibc host dies on a musl host with "no such file or directory" — why?

level: seniorimportance: must knowfreq 55%

basics

~20 s

The binary was linked with cgo enabled, so it is dynamically linked and names a glibc dynamic loader such as /lib64/ld-linux-x86-64.so.2. That path does not exist on a musl host, so exec fails and reports the missing interpreter.

open as a page

Why does a Go file named watch_linux.go disappear from a windows/amd64 build?

level: middleimportance: should knowfreq 44%

basics

~20 s

The go tool reads the platform out of the filename: a file ending in _linux.go is compiled only when GOOS=linux. Building for Windows drops it from the package entirely, so every symbol it declared becomes undefined.

open as a page

Why would go build -ldflags -X silently leave a Go string variable empty at run time?

level: middleimportance: should knowfreq 58%

basics

~20 s

The Go linker matches -X against a package-level string variable by its full path-qualified name and does nothing at all when nothing matches. A typo, a missing module path prefix, a const, or a non-constant initialiser each leaves the value untouched, with no error.

open as a page

What do the netgo and osusergo build tags do when cgo stays enabled?

level: middleimportance: should knowfreq 42%

basics

~20 s

Both are build tags that force a pure-Go implementation even with cgo enabled: netgo pins the net package to its Go hostname resolver, osusergo pins os/user to the version parsing /etc/passwd. Neither makes the binary static.

open as a page

Your Go CLI ships prebuilt binaries — how do you pick the GOOS/GOARCH matrix and verify each artifact?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Ship the pairs your users actually run — usually linux, darwin and windows on amd64 and arm64 — confirm each against go tool dist list, keep the default microarchitecture baseline, and smoke-run every artifact on the real target before publishing.

open as a page

How do you stamp a Go binary so a postmortem can prove which build was running?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Use two stamps: -ldflags -X for a human-readable version string the program logs at start-up, and the go command's automatic version-control stamping for the revision. Read them back with go version -m on the artefact, or from inside the process.

open as a page

How do you decide whether CGO_ENABLED=0 is mandatory for every shipped binary when one team's dependency needs cgo?

level: principalimportance: should knowfreq 32%

basics

~20 s

Default to CGO_ENABLED=0, because it makes the artifact independent of the host C library, then run an explicit exception process for services that need cgo: a named owner, a build environment pinned to the target libc, and verified lookups.

open as a page

What do Go's GOAMD64 and GOARM settings control, and what breaks if you raise them?

level: middleimportance: nice to knowfreq 28%

basics

~20 s

They select the instruction-set baseline inside one GOARCH. GOAMD64 accepts v1 (the default) through v4, and GOARM picks the 32-bit ARM level 5, 6 or 7. Raise the baseline and older CPUs die at run time with an illegal instruction.

open as a page

What do the -s and -w linker flags do in go build -ldflags, and what do they cost?

level: middleimportance: nice to knowfreq 40%

basics

~20 s

The -w flag tells the Go linker to omit the DWARF debug information and -s to omit the symbol table as well, which cuts binary size substantially. The cost is symbolic debugging: a debugger can no longer map addresses back to source.

open as a page

What does building with -ldflags '-extldflags "-static"' do, and why does glibc warn about getaddrinfo?

level: seniorimportance: nice to knowfreq 25%

basics

~20 s

It forwards -static to the external C linker, so a cgo build links the C library into the executable instead of loading it at startup. glibc still opens its lookup modules at run time, so it warns.

open as a page