skip to content

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

level: middleimportance: nice to knowfreq 28%

answer

  1. one architecture, several CPU generations
  2. a second variable beside GOARCH
  3. v1 is the safe default
  4. the crash is a signal, not a build error
  5. illegal instruction on the oldest host

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.

solid answer

~50 s

`GOARCH=amd64` is a family, not a single CPU, so Go has a second knob for the instruction-set baseline within it. `GOAMD64` takes `v1` through `v4`: `v1` is the original 64-bit baseline and the default, `v2` adds SSE4.2 and POPCNT, `v3` adds AVX2, BMI and FMA, `v4` adds AVX-512. `GOARM` does the equivalent for 32-bit ARM with the values `5`, `6` and `7`, differing mainly in the floating-point hardware assumed, and `GOARM64` plays that role for 64-bit ARM. Raising the level lets the compiler emit newer instructions, so the binary stops being universal: on a CPU without them the process dies at run time with `SIGILL: illegal instruction`, often inside a vectorised standard library routine, which makes the crash look unrelated. For a public binary keep the default; for a known fleet, raise it only after checking the oldest CPU and measuring a real gain.

code

text · 5 lines
text
GOAMD64=v3 go build ./cmd/render   # allows AVX2, BMI, FMA

# same binary on a pre-AVX2 server:
# SIGILL: illegal instruction
# (no error at load time, and none on the build machine)

go deeper

for a junior

Know that GOARCH names a whole family of CPUs and that Go has a separate setting for how modern a CPU within it you are willing to require. Recognise the names GOAMD64 and GOARM.

for a middle

Be able to list the GOAMD64 levels with the default, say what GOARM's 5/6/7 distinguish, and explain that the compiler applies the level to standard library code as well as yours.

for a senior

Talk through the diagnosis: an illegal-instruction signal on one host class, no build or load error, a stack in library code, and the check of the oldest CPU model in the fleet against the level the artifact was built with.

for a principal

Own the policy — one conservative public artifact versus a tuned internal one, what evidence justifies raising a baseline, and how the choice is recorded so a future crash on old hardware is traceable to it.

## GOARCH is a family, not a chip `GOARCH=amd64` covers every 64-bit x86 processor from 2003 onward, and those chips differ enormously in what instructions they provide. If the compiler always assumed the newest ones, binaries would crash on older machines; if it always assumed the oldest, it could never use AVX2 or FMA. Go resolves this with a second, per-architecture variable that sets the **microarchitecture baseline**. ## GOAMD64 `GOAMD64` selects one of four industry-standard x86-64 levels: - **`v1`** — the original 64-bit baseline (SSE2). **This is the default.** Anything that runs 64-bit x86 code runs a `v1` binary. - **`v2`** — adds the mid-2000s set, including SSE3, SSE4.2 and POPCNT. - **`v3`** — adds AVX, AVX2, BMI1/BMI2 and FMA (roughly Haswell-era and later). - **`v4`** — adds AVX-512. The compiler is then free to emit instructions from that level anywhere it helps — in your code and in the standard library, which is recompiled for the same settings. ## GOARM and GOARM64 For `GOARCH=arm` (32-bit), `GOARM` takes `5`, `6` or `7`, which mainly determines what floating-point hardware the compiler may assume — `5` targets machines with no hardware FPU and uses software floating point, while `6` and `7` assume increasingly capable VFP units. This matters on small devices: a binary built for `7` will not run on an older `5`-class board. For `GOARCH=arm64`, `GOARM64` (added in Go 1.23) serves the same role, defaulting to the conservative `v8.0` baseline. There is a parallel variable for RISC-V. The pattern is the same everywhere: `GOARCH` chooses the instruction set, the extra variable chooses how much of it you are allowed to use. ## The failure mode, and why it is confusing Nothing checks the baseline when the binary starts. There is no header field the loader validates and no friendly "this CPU is too old" message. The program launches, runs, and dies the first time control reaches a compiled path that actually uses an unsupported instruction: ``` SIGILL: illegal instruction ``` Three things make this hard to diagnose in production: 1. **It is late and data-dependent.** The offending path may only be reached under certain input, so the crash can appear hours after deploy or only on one host. 2. **The stack often points at the standard library.** The compiler vectorises library routines too, so the trace lands somewhere you did not write and did not change. 3. **It is invisible on the build machine**, which is usually newer than the oldest production host. The corresponding failure for a *wrong architecture* is much friendlier: the kernel refuses to load it at all (`Exec format error` on Linux). Microarchitecture mismatches have no such gate. ## When raising the level is justified The defaults are conservative on purpose: distribution safety beats a few percent of throughput for anything you hand to strangers. Raising the baseline is defensible when all three hold: - You **know the oldest CPU** that will ever execute the artifact — a homogeneous fleet you control, or an image pinned to one instance family. - You have **measured** a gain on a representative workload. Numeric and byte-processing code can benefit meaningfully; a service dominated by I/O or allocation usually does not. - The artifact is **not** the same one you publish publicly. If you ship one binary to unknown users, it stays at the default. Record the choice where the next person will find it — in the build script and in release notes — because the failure it can cause looks nothing like a build setting. ## Do not confuse the two knobs `GOARCH` decides *which machine* the binary is for; the microarchitecture variable decides *which generation of that machine*. Getting `GOARCH` wrong yields a file the target cannot even load. Getting the microarchitecture wrong yields a file that loads, runs, and then kills the process at the worst possible moment.

  • At which point does an unsupported-instruction mismatch surface — build, load, or run?
    At run time, the first time execution reaches a compiled path that uses the instruction. The kernel does not validate the baseline at load, so the process starts normally and then dies with `SIGILL: illegal instruction`. Because the compiler also applies the level to standard library code, the stack trace frequently points somewhere you never touched.
  • What is the default GOAMD64, and why is it that conservative?
    `v1`, the original 64-bit x86 baseline. It guarantees the binary runs on every amd64 CPU Go supports, which is the right trade for anything you distribute to users whose hardware you cannot see. Higher levels are opt-in because the gain is workload-dependent while the risk — a run-time crash on someone's older machine — is unconditional.
  • Does GOARCH=arm64 have an equivalent setting?
    Yes: `GOARM64`, added in Go 1.23, which defaults to the baseline `v8.0` and can request newer profiles or optional extensions. Its behaviour matches GOAMD64 — the binary stops being universal for the architecture, and a chip lacking the requested features fails at run time rather than at load.

saying these in an interview costs you the question

  • Thinks GOARCH=amd64 already implies a modern CPU with AVX2
  • Expects an unsupported instruction to be rejected at build or load time
  • Raises GOAMD64 fleet-wide for speed without knowing the oldest CPU
  • Confuses GOAMD64 with GOARCH and treats v3 as a separate architecture
  • Assumes a higher baseline always makes the program measurably faster