In Go, what does uint8(n) give when n is 300, and why is that dangerous in a parser?
answer
- the conversion never fails
- keeps the low bits, drops the rest
- 300 in one byte is not 255
- only literals are checked
- validate before you narrow, not after
basics
~20 sIt gives 44. A Go conversion between integer types never fails: it keeps the low bits and discards the rest. In a parser a hostile large value silently becomes a small plausible one that passes validation.
solid answer
~50 sA numeric conversion in Go is a reinterpretation, not a checked cast: `uint8(n)` for an `int` variable holding 300 keeps the low eight bits and yields 44, and `int8(n)` for 200 yields -56. There is no error, no panic, and no vet diagnostic — only *constant* conversions such as the literal `uint8(300)` are rejected at compile time. In a parser reading untrusted input that turns a range check into a lie: a CIDR field of `/264` truncates to `/8`, and the scanner that was meant to walk 256 addresses walks sixteen million. The fix is to validate before narrowing — bound-check against `math.MaxUint8`, or parse at the target width with `strconv.ParseUint(s, 10, 8)`, which returns an out-of-range error instead of truncating. To find existing cases, write a fuzz target with `testing.F` asserting an invariant on the parsed value; boundary inputs like 256 surface within seconds.
code
go · 8 linesbits, err := strconv.Atoi(field) // field = "264"
if err != nil {
return err
}
prefix := uint8(bits) // silently 8
if prefix > 32 {
return errBadPrefix // never reached
}go deeper
Recall the core fact: converting between integer types keeps the low bits and never reports a problem, so uint8 of 300 is 44 and not an error.
Explain the bit-level result for both signedness directions and the constant-versus-variable asymmetry — why the literal form is a compile error while the variable form compiles silently.
Show where the check belongs relative to the conversion, name the parsing functions that carry a width, and describe how you would hunt for existing cases with a fuzz target and boundary-value tests rather than hoping a linter finds them.
Own the policy for parsing untrusted input across services: which types cross the boundary, whether decoding is hand-rolled or generated, and how you fund a sweep of existing parsers once one such truncation has been found.
## What a Go conversion actually does `T(x)` between integer types is defined as taking the value and, if the destination is narrower, discarding the high bits; the remaining bits are then interpreted according to the destination's signedness. It is not a cast that can fail. It does not clamp. It does not report anything. ```go n := 300 fmt.Println(uint8(n)) // 44 (300 - 256) m := 200 fmt.Println(int8(m)) // -56 (0xC8 read as signed) var big int64 = 1 << 33 fmt.Println(int32(big)) // 0 (every surviving bit is zero) ``` The last one is the nastiest: the truncated value is not merely wrong, it is *innocuous*. Zero, or 44, or 8 — small numbers that pass every sanity check written downstream. ## The constant exception, and why it hides the bug Go does check conversions of untyped constants. `uint8(300)` written literally is a compile error, because the constant must be representable in the destination type. So the naive form of this mistake never gets committed — the compiler catches it the first time. What gets committed is the same conversion applied to a *variable*, and that is exactly the case where the value came from outside the process. The compiler is silent, `go vet` has no narrowing-conversion analyzer (its `shift` check will flag a shift wider than the type, but nothing watches conversions), and the code reads as if a check has happened because a type name appears. ## The parser scenario Consider a scanner that walks CIDR ranges supplied by a caller, and parses `address/bits` by hand: ```go bits, err := strconv.Atoi(field) // field = "264" if err != nil { return err } prefix := uint8(bits) // 264 becomes 8 if prefix > 32 { return errBadPrefix } ``` The validation is there. It is simply validating the wrong number. `264` passes as `8`, and the range that should have been rejected expands to sixteen million addresses — a scan the operator never asked for, aimed at hosts they may not own. Every variation of this shape exists in the wild: a length prefix truncated so a bounds check passes while the real body is longer, a count truncated to zero so a loop never runs, a port number truncated into a different service. Note the ordering error at the heart of it: the code converts and *then* validates. Once the conversion has happened the evidence is gone. ## Getting it right **Validate in the wide type, before narrowing.** ```go if bits < 0 || bits > 32 { return errBadPrefix } prefix := uint8(bits) ``` **Or parse directly at the destination width.** `strconv.ParseUint(s, 10, 8)` returns a `uint64` but rejects anything that would not fit in eight bits with an `ErrRange` error, so the range check and the parse are the same step and cannot drift apart. `strconv.ParseInt(s, 10, 16)` does the same for signed 16-bit fields. **Or do not narrow at all.** Carry the wide type end to end and let the struct field be `int` unless a fixed width is genuinely part of the format. The narrowing then happens once, at the encoder, where the value is already known to be in range. **Compare against the named limits** — `math.MaxUint8`, `math.MaxInt32`, `math.MaxUint32` — rather than open-coded magic numbers, so the check is obviously tied to the destination type. ## Finding the ones already in the tree This is where fuzzing earns its keep, because the failing inputs are boundary values that a hand-written table test almost never contains — 255, 256, 257, 65535, 65536, and the negative mirrors. ```go func FuzzParsePrefix(f *testing.F) { f.Add("10.0.0.0/8") f.Fuzz(func(t *testing.T, s string) { p, err := parsePrefix(s) if err == nil && p.bits > 32 { t.Fatalf("accepted %q as /%d", s, p.bits) } }) } ``` Run with `go test -fuzz=FuzzParsePrefix`. The invariant is the important part: assert something that must hold for every accepted input — a parsed length never exceeds the input length, a re-encode round-trips, an accepted prefix is within range. A fuzz target with no invariant only finds panics, and truncation does not panic. Failing inputs are written into `testdata/fuzz/`, so the corpus becomes a permanent regression test. The `-race` detector is no help here (it finds data races, not value errors), and `go build -gcflags=-m` reports escape analysis, not conversions. ## The reviewer's heuristic When reviewing a parser for hostile input, grep for narrowing conversions and ask one question at each: *where is the check, and is it before or after this line?* A conversion applied to anything derived from the network, a file, a header or a query parameter is a validation site until proven otherwise. And prefer parsing functions that carry the width — `strconv.ParseUint(s, 10, 8)` over `strconv.Atoi` plus a cast — so the check cannot be forgotten in the first place.
- Why does the compiler reject uint8(300) but accept uint8(n) where n holds 300?300 written literally is an untyped constant, evaluated at compile time with arbitrary precision, and the spec requires a converted constant to be representable in the destination type — so it is an error. A variable's value is not known at compile time, so the conversion becomes a machine instruction that keeps the low bits. That asymmetry is precisely why the dangerous form is the one that survives to production.
- Would go vet or the race detector catch this?Neither. go vet ships no narrowing-conversion analyzer — its shift check flags a shift wider than the operand's type, which is a different mistake — and the race detector reports concurrent unsynchronised access, not wrong values. The tools that do work are a fuzz target with an invariant, boundary-value table tests around 255, 256, 65535 and 65536, and code review focused on where the range check sits relative to the conversion.
- What invariant would you assert in the fuzz target?Something that must hold for every input the parser accepts, not merely absence of panic. For a length prefix: the parsed length never exceeds the remaining input. For a CIDR prefix: an accepted value is within 0..32. For a record: re-encoding the parsed form reproduces the accepted bytes. Without such an assertion the fuzzer only finds crashes, and a silent truncation does not crash.
It is pouring a litre into a 250ml measure. Nothing objects, nothing overflows onto the bench in a way you will notice later — you are simply left holding a quarter of a litre and a reading that looks reasonable.
saying these in an interview costs you the question
- Expects a Go conversion to panic or return an error when the value does not fit
- Thinks a narrowing conversion saturates at the destination's maximum
- Believes go vet or the compiler flags narrowing conversions on variables
- Validates the value after converting rather than before
- Assumes converting from int64 to int is always lossless