How wide are Go's int, uint and uintptr when you build for GOARCH=386 or arm?
answer
- one type, two widths
- the build target decides
- sized types stay put, one family moves
- int64 into int on a 32-bit build
- no panic and no vet warning
basics
~20 sGo'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 sThe 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 linesvar 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
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.
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.
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.
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