skip to content

In Go, what happens when an int8 or a uint8 exceeds its maximum value?

level: juniorimportance: must knowfreq 70%

answer

  1. the odometer rolls over
  2. no exception, no widening
  3. 127 plus one is not 128
  4. the compiler only stops constants
  5. high bits are simply discarded

basics

~20 s

Go integers wrap around silently: an int8 holding 127 becomes -128 when incremented, and a uint8 holding 0 becomes 255 when decremented. There is no panic and no promotion to a wider type. Only constant overflow is caught at compile time.

solid answer

~50 s

Go's integer types have fixed widths, and arithmetic that does not fit simply discards the high bits — two's-complement wrap-around. `var x int8 = 127; x++` leaves x at -128; `var u uint8 = 0; u--` leaves u at 255. Nothing panics and nothing widens to a bigger type on the way. Crucially this is *defined* behaviour in the Go spec, not undefined behaviour as signed overflow is in C, so the result is deterministic across platforms. The compiler only catches overflow in constant expressions, which are evaluated with arbitrary precision and must be representable in their target type: `var x int8 = 200` and `uint8(300)` fail to compile, while the same values reaching those types through variables wrap silently. If you need detection, bound-check against `math.MaxInt8` and friends before the operation, or use `math/bits` helpers like `bits.Add64`, which returns a carry out.

code

go · 9 lines
go
var x int8 = 127
x++
fmt.Println(x) // prints: -128

var u uint8 = 0
u--
fmt.Println(u) // prints: 255

// var y int8 = 200 // does not compile: the constant is out of range

go deeper

for a junior

Be ready to state the rule in one line and give the example without pausing: an int8 at 127 goes to -128, a uint8 at 0 goes to 255, and nothing complains.

for a middle

Explain the mechanism: fixed width, high bits discarded, two's complement. Know that constants are range-checked at compile time while variables are not, and that Go defines the result rather than leaving it undefined.

for a senior

Show where you place the guard in real code — bound-check at the boundary where an outside value arrives, compute in the wide type, and reach for math/bits when you need a carry. Name a counter or wire field where wrap has actually bitten you.

for a principal

Own the house rule: which widths the team uses for identifiers, sequence numbers and sizes in stored and wire formats, and what it costs to widen one later. Decide when deliberate modular arithmetic is a feature rather than a bug.

## Fixed width is the whole story Every Go integer type has a fixed size in bits. `int8`, `int16`, `int32` and `int64` are signed; `uint8`, `uint16`, `uint32` and `uint64` are unsigned; `int` and `uint` are one machine word wide on the target platform (64 bits on ordinary 64-bit builds); and `uintptr` is an unsigned integer wide enough to hold a pointer's bit pattern. `byte` is another name for `uint8` and `rune` is another name for `int32`. Because the width is fixed, a value physically cannot leave its range, so when an operation produces a result that does not fit, the bits above the width are simply thrown away. That is what "wrap-around" means. An `int8` holds values -128 through 127. Adding 1 to 127 gives the bit pattern `1000 0000`, which read as a two's-complement signed byte is -128. A `uint8` holds 0 through 255; subtracting 1 from 0 gives `1111 1111`, which is 255. An `int32` at 2147483647 rolls to -2147483648. A `uint16` at 65535 rolls to 0. ## Defined, not undefined This matters more than it first looks. In C, signed integer overflow is undefined behaviour, and optimising compilers are allowed to assume it never happens — which is how loops get deleted and bounds checks vanish. Go's specification instead says that these operations may overflow and that the resulting value exists: it is computed by discarding the high bits. So the answer is deterministic, identical on every architecture Go targets, and safe to reason about. (One curiosity that falls out of the same rule: for the most negative value of a signed type, dividing by -1 yields that same most negative value back, because the mathematically correct answer does not fit.) Wrap-around is not only a hazard — it is the point of the unsigned types. Hash mixing, checksums and pseudo-random generators are all built on deliberate modular arithmetic, and Go gives them well-defined semantics to build on. ## Where the compiler helps, and where it stops Go has untyped constants that are evaluated at compile time with arbitrary precision, and a constant must be *representable* in the type it lands in. So these are compile errors, caught before anything runs: - `var x int8 = 200` - `const big = 1 << 40; var y int32 = big` - `uint8(300)` — a conversion of a constant Once a value lives in a variable, that safety net is gone. `n := 300; b := uint8(n)` compiles happily and stores 44. The compiler emits a plain machine instruction with no check attached, because a check on every add would cost real performance in the common case where overflow cannot happen. ## There is no runtime check at all Go does not panic on integer overflow, does not set an inspectable flag, and does not return an error. The only arithmetic operation that panics is an integer division by zero. Overflow is not one of them. A counter declared `uint32` that ticks past 4294967295 quietly restarts at 0, and every downstream comparison keeps working on the wrong number. ## Detecting it when you need to Three practical techniques: 1. **Bound-check before the operation** against the `math` package limits — `math.MaxInt8`, `math.MaxInt32`, `math.MaxUint32`, `math.MaxInt64`, `math.MaxInt`. For addition of non-negative values: `if a > math.MaxInt64-b { return errOverflow }`. 2. **Compute in a wider type, then narrow after checking.** Multiply two `int32` values into an `int64`, verify the product fits, and only then convert back. 3. **Use `math/bits`.** `bits.Add64` and `bits.Sub64` return a carry/borrow alongside the result, and `bits.Mul64` returns the high and low halves of a full 128-bit product; a non-zero carry or high word *is* the overflow signal. The `min` and `max` builtins added in Go 1.21 are worth knowing here, but they solve a neighbouring problem: they pick the smallest or largest of their arguments, on any ordered type, so they help you *clamp* a value you already have. They cannot tell you that an addition already wrapped. ## What to carry into review The places wrap actually bites are boundaries and counters: a monotonic sequence number pinned to a 32-bit wire field, a duration in nanoseconds stuffed into an `int32`, a size or length that came from outside the process. Pick the width from the range the value can actually take, keep the arithmetic in the wide type, and check once at the edge rather than hoping the runtime will tell you.

  • Why does `var x int8 = 200` fail to compile while an int8 variable can reach 200's bit pattern at runtime?
    The 200 is an untyped constant, evaluated at compile time with arbitrary precision, and the spec requires it to be representable in the type it is assigned to — 200 does not fit in int8's -128..127 range, so it is an error. Runtime arithmetic on variables is a plain machine instruction with no representability check, so it wraps instead.
  • How would you detect an overflow before it happens?
    Bound-check against the limit before operating — for example `if a > math.MaxInt64-b` before `a+b` — or do the arithmetic in a wider type and verify the result fits before narrowing. For the widest types, `math/bits` gives you `bits.Add64` and `bits.Sub64`, which return a carry or borrow, and `bits.Mul64`, whose high word is non-zero exactly when the product overflowed 64 bits.
  • Do the min and max builtins help with overflow?
    No — they clamp, they do not detect. `min` and `max` were added in Go 1.21, are variadic, and work on any ordered type including strings and floats, unlike `math.Max`, which is float64-only. They are useful for capping a value into range before you convert it, but by the time an addition has wrapped there is nothing left for them to see.

It is a car odometer with a fixed number of dials. Pass the last number and it rolls quietly back to zero rather than growing an extra dial or refusing to move.

saying these in an interview costs you the question

  • Says Go panics or errors on integer overflow
  • Calls Go's signed overflow undefined behaviour, as in C
  • Expects operands to be promoted automatically to a wider type
  • Believes the compiler catches overflow in variable arithmetic
  • Thinks unsigned subtraction below zero clamps at zero