skip to content

Word Size and Endianness

int and uintptr are 32 bits on some targets and 64 on others, and a 64-bit counter a 32-bit build updates atomically has to be 8-byte aligned or it panics on the first access.

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

questions

5

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

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

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

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