skip to content

Non-Native Build Targets

Not every build produces a native binary for the machine in front of you, and the target you pick changes what the code may assume about threads, word size and byte order.

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

explore

questions

11

How wide are Go's int, uint and uintptr when you build for GOARCH=386 or arm?

level: juniorimportance: must knowfreq 48%

answer

  1. one type, two widths
  2. the build target decides
  3. sized types stay put, one family moves
  4. int64 into int on a 32-bit build
  5. no panic and no vet warning

basics

~20 s

Go's int and uint are 64 bits on amd64 and arm64 but only 32 bits on 386, arm and the other 32-bit targets, and uintptr is always pointer-sized. The sized types, int8 through int64 and float64, never change.

solid answer

~50 s

The Go spec makes `int` and `uint` implementation-defined, the same size as each other and at least 32 bits, and on the gc toolchain that size follows the build target: 64 bits for amd64 and arm64, 32 bits for 386, arm, mips and mipsle. `uintptr` is separately defined as wide enough to hold a pointer's bits, so it shrinks on the same targets. Everything sized — `int8` to `int64`, `float64`, `rune`, `time.Duration` — is identical everywhere. The consequences on a 32-bit build are that `len` returns `int` so no slice or string can exceed about 2^31-1 elements, that `math.MaxInt` shrinks, and above all that converting an `int64` byte count, offset or timestamp to `int` truncates silently: no panic, no vet warning. Keep sizes and timestamps in explicit `int64` and narrow only after a checked comparison.

code

go · 9 lines
go
var total int64 = 5 << 30 // 5 GiB of collected bytes

// exact on GOARCH=amd64; the high bits are dropped on GOARCH=386 or arm
report(int(total))

// portable: stay in int64, narrow only after a checked comparison
if total > int64(math.MaxInt) {
	return fmt.Errorf("total %d does not fit in int on this build", total)
}

go deeper

for a junior

Be ready to say plainly that int is 32 bits on 32-bit targets and 64 on 64-bit ones, while int64 is 64 everywhere. Knowing to reach for int64 when a value is a byte count or a timestamp is most of what is expected here.

for a middle

Explain the mechanics: which types follow the target, what the spec actually guarantees, why len returning int caps slice size, and why an int64-to-int conversion truncates instead of failing.

for a senior

Show the production angle. Point at the conversions that silently lose data on a cross-compiled build, say why nothing at runtime reports them, and describe the CI leg that would have caught it.

for a principal

Own the convention. Decide that sizes, offsets and timestamps are int64 in every exported signature, and weigh that consistency against the churn of changing existing APIs across the codebase.

## Two integer families, not one Go's numeric types split into two groups, and the split is the whole answer. The **sized** types carry their width in the name and have that width on every target: `int8`, `int16`, `int32`, `int64`, the `uint8`–`uint64` twins, `float32`, `float64`. `byte` is an alias for `uint8` and `rune` an alias for `int32`, so they are sized too. `time.Duration` is defined as `int64`, so a duration is 64 bits even on a 32-bit device. The **unsized** types are `int` and `uint`. The spec says only that they are implementation-specific, the same size as each other, and at least 32 bits. On the standard gc toolchain that size is the target's word size: 64 bits on amd64, arm64, riscv64, ppc64le, s390x and loong64; 32 bits on 386, arm, mips and mipsle. Note that `int` and `int32` are *distinct types* even when they are the same width — Go has no implicit conversion between them, which is why portable code that mixes them needs explicit casts and why those casts are where the bug lives. `uintptr` is a third thing again: an unsigned integer defined to be large enough to hold the bit pattern of any pointer. It is 4 bytes on 386 and arm, 8 bytes on amd64 and arm64. ## What actually changes when GOARCH goes 32-bit - `len` and `cap` return `int`, so the largest possible slice, string or map on a 32-bit build holds roughly 2^31-1 elements. A 3 GiB `[]byte` is not merely slow to allocate, it is unrepresentable. - `math.MaxInt` and `math.MinInt` change value, so any comparison against them means something different per target. - An untyped constant too large for `int` stops compiling. `const limit = 1 << 40` assigned into an `int` is a build error under `GOARCH=386` — a friendly failure, because it happens before the binary ships. - A **variable** conversion, `int(someInt64)`, is legal on every target and simply discards the high bits on a 32-bit one. There is no panic, no error return, and no vet diagnostic. This is the silent half of the problem and it is the half that reaches production. - Pointer-sized things shrink with the pointer: the fields inside a slice header, an interface value, a map or channel value. The usable address space shrinks too, which is a separate ceiling on a memory-hungry process. What does *not* change: `int64`, `uint64`, `float64`, `rune`, `byte`, `time.Duration`, and the layout of anything built only out of them. ## Where it bites in real code A daemon that collects and reports counters is dense with 64-bit quantities: byte totals, nanosecond timestamps (`time.Now().UnixNano()` is around 1.7×10^18 and is `int64`), file offsets from `os.File.Seek`, monotonic durations, identifiers from a database. Each one is fine as long as it stays `int64`. The moment somebody writes `int(total)` — to fit an older function signature, to index something, to feed a struct field declared `int` years ago — the 64-bit build keeps working perfectly and the 32-bit build starts reporting numbers that are too small, with no crash to point at. ## Writing it portably Use `int` for what it is for: lengths, capacities and indices, which are `int` by definition in Go. Use explicit `int64` for sizes, offsets, byte counts, timestamps and external identifiers, including in exported struct fields, so no caller has to guess. When a narrow value is genuinely required, narrow deliberately: ```go if total > int64(math.MaxInt) { return fmt.Errorf("total %d does not fit in int on this build", total) } ``` If you need the width itself, `strconv.IntSize` and `math/bits.UintSize` are constants holding 32 or 64, usable in a compile-time guard or a test assertion. ## Proving it Building for the target catches the constant-overflow class immediately: `GOARCH=386 go build ./...` and `GOARCH=386 go vet ./...` type-check the whole package under 32-bit widths. The silent truncation class needs execution — a test that pushes a value above 2^31 through the real code path, run with `GOARCH=386 go test ./...`. A target you compile but never run is not a target you have tested.

  • Is int guaranteed to be the same width as a pointer in Go?
    No. The spec says only that `int` and `uint` are implementation-specific, the same size as each other, and at least 32 bits. The type defined to hold a pointer's bits is `uintptr`. On the common gc targets `int` happens to match the pointer width, but treat that as a property of those targets rather than a rule, and never stash a pointer in an `int`.
  • Besides int itself, what else shrinks on a 32-bit Go target?
    Anything typed `int` or pointer-sized: `len` and `cap` results, slice and map indices, the length and capacity fields inside a slice header, and every value derived from them. The practical ceiling is that one slice or string cannot exceed roughly 2^31-1 elements. `time.Duration` stays `int64`, and the sized numeric types are unchanged.
  • How would you catch an int-width assumption at compile time rather than in the field?
    Build and vet for each supported target: `GOARCH=386 go build ./...` rejects untyped constants that overflow `int`, and comparisons against `math.MaxInt` change meaning under it. `strconv.IntSize` or `math/bits.UintSize` lets a test assert the width. A variable `int64`-to-`int` conversion stays legal everywhere, so that one needs an explicit range check rather than a compiler.

int is a shipping container whose size the factory picks per destination. The label is the same everywhere; how much fits inside depends on which target the build was made for.

saying these in an interview costs you the question

  • Says Go's int is always 64 bits
  • Treats int and int64 as the same type
  • Converts an int64 file size to int without a range check
  • Assumes uintptr is always 8 bytes
  • Thinks GOARCH only changes instructions, not type widths
  • Expects a runtime panic when an int64 truncates
open as a page

What does building with GOOS=js GOARCH=wasm produce, and why does the page also need wasm_exec.js?

level: juniorimportance: must knowfreq 35%

basics

~20 s

It produces a .wasm module, not a standalone program. The module declares imports for the host functions the Go runtime needs, and wasm_exec.js is the JavaScript glue that ships with the toolchain, supplies those imports and starts the program.

open as a page

Why does atomic.AddUint64 on a struct field panic on GOARCH=386 but not on amd64?

level: middleimportance: should knowfreq 32%

basics

~20 s

On 32-bit targets a 64-bit struct field only needs 4-byte alignment, while the 64-bit atomic functions require an 8-byte-aligned address, so they panic there. On 64-bit targets the field lands aligned anyway, hiding the bug.

open as a page

When is binary.NativeEndian the right choice in Go, and when is it a portability bug?

level: middleimportance: should knowfreq 30%

basics

~20 s

binary.NativeEndian encodes in whatever byte order the build target uses, so it fits only bytes that never leave the machine. Anything written to a wire or a shared file must name binary.BigEndian or binary.LittleEndian instead.

open as a page

How do you expose a Go function to JavaScript with syscall/js, and why must main not return?

level: middleimportance: should knowfreq 30%

basics

~20 s

Wrap the Go function with js.FuncOf and attach it to a JavaScript object, usually via js.Global().Set. The program exits when main returns, and once it has exited the wrapper can no longer be called, so a callback-serving module blocks forever.

open as a page

What does GOOS=wasip1 GOARCH=wasm produce, and what can that module no longer do at run time?

level: middleimportance: should knowfreq 24%

basics

~20 s

It produces a WebAssembly module for WASI preview 1 hosts; the host runtime supplies the imports, so no JavaScript glue. The program sees only directories the host preopened, cannot dial out, and has no threads or subprocesses.

open as a page

Your metrics daemon panics only on the 32-bit ARM build — how do you diagnose it and stop the class recurring?

level: seniorimportance: should knowfreq 25%

basics

~20 s

Read the panic: an unaligned 64-bit atomic points at a 64-bit struct field on a 32-bit target. Then hunt its silent siblings, int truncation above all, and add a 32-bit GOARCH build, vet and test leg to CI.

open as a page

A Go js/wasm callback waits on a channel, the tab freezes, and the console logs that all goroutines are asleep. Why?

level: seniorimportance: should knowfreq 28%

basics

~20 s

The browser target has one thread, shared with the page's event loop, and a call from JavaScript into Go holds that loop until it returns. Anything the callback waits on needs a later browser event, which can never run.

open as a page

What do the go:wasmimport and go:wasmexport directives do, and why are their parameter types restricted?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

They wire a Go WebAssembly module to its host without cgo: go:wasmimport binds a bodyless declaration to a host function, and go:wasmexport publishes a Go function for the host to call. Types are limited because WebAssembly signatures carry only numbers.

open as a page

How do you decide which GOARCH targets your Go service will keep supporting?

level: principalimportance: nice to knowfreq 18%

basics

~20 s

Treat each supported GOARCH as a standing cost: a CI leg, a constraint on every struct and counter, and a bug class you must keep testing for. Decide from fleet telemetry and a dated, tiered support matrix.

open as a page

How do you decide whether to ship a Go library to the browser as wasm when another team owns the bundle budget?

level: principalimportance: nice to knowfreq 26%

basics

~20 s

Decide on measured cost against a named risk: reuse is worth megabytes of download only when two implementations disagreeing would itself be a defect. Bring the compressed size, a fallback plan, and an owner for rebuilds.

open as a page