Why does Go use int for len and counts rather than uint, and when does unsigned bite?
answer
- what is below zero for a uint
- three minus five, in a byte
- the loop that never ends
- no implicit conversions in Go
- unsigned is for bits, not for optimism
basics
~20 sUnsigned arithmetic wraps at zero, so subtracting a larger value gives a huge positive number instead of a negative one. Go returns int from len and cap so index arithmetic can go negative and be caught by an ordinary check.
solid answer
~50 sTwo reasons, and both are about arithmetic rather than about whether a count can be negative. First, unsigned types have no room below zero: `var a, b uint8 = 3, 5` makes `a - b` equal 254, and a countdown loop written `for i := uint(2); i >= 0; i--` never terminates, because the condition is true by construction and 0 minus 1 wraps to the largest uint. Signed indices let intermediate results go negative and be caught by a plain comparison. Second, Go has no implicit numeric conversion, so an unsigned count forces `int(x)` conversions at every boundary with `len`, `cap`, slice indexing and the standard library — and every one of those conversions is a place where a value can silently truncate. Reach for unsigned when you want modular arithmetic or bit manipulation: masks, hashes, checksums, fixed-width wire fields. Not merely because a value "can never be negative".
code
go · 7 linesvar a, b uint8 = 3, 5
fmt.Println(a - b) // prints: 254
// never terminates: i >= 0 is always true, and 0-1 wraps to the max uint
for i := uint(2); i >= 0; i-- {
_ = i
}go deeper
Know that len and cap return int and that indices are int, so you rarely need another integer type when working with slices and strings.
Explain both supports for the design: unsigned arithmetic wraps at zero so negative intermediates vanish, and Go's lack of implicit conversion makes an unsigned field cost a cast at every boundary. Have the 3 minus 5 example ready.
Demonstrate the judgment call in a real struct: which fields are genuinely fixed-width representation and belong unsigned, which are counts and stay int, and where you validate range once at the edge instead of encoding it in the type.
Own the convention for the codebase's data types and stored formats, including what it costs to change a field's signedness once other services parse it, and how you keep validation at the boundary rather than scattered through the call graph.
## The rule Go chose `len` and `cap` return `int`. Slice and array indices are `int`. Most standard-library sizes, offsets and counts are `int`. That is a deliberate design decision, and Go is not alone in later regretting the opposite choice — C++'s unsigned `size_t` is widely considered a mistake for exactly the reasons below. ## Reason one: unsigned has no room below zero Every integer type in Go wraps on overflow, and for an unsigned type the interesting edge is at zero rather than at the top. Two consequences show up constantly: ```go var a, b uint8 = 3, 5 fmt.Println(a - b) // prints: 254 ``` The mathematically correct -2 does not exist in `uint8`, so the result is 256-2. Nothing warns you. If that value is then used as a length or an index, you have turned a small logic slip into a very large number. The classic loop version is worse, because it hangs rather than misbehaves: ```go for i := uint(2); i >= 0; i-- { // i >= 0 is always true for an unsigned type } ``` The compiler accepts it. The condition can never be false, and when `i` reaches 0 the next decrement wraps it to the maximum `uint`. Written with `int`, the same loop terminates because `i` is allowed to become -1. The general shape of the problem: intermediate results in index arithmetic *want* to be negative. `lo - 1`, `mid - 1`, `end - start` in the wrong order, `pos - offset` when the offset is bigger. With signed arithmetic those produce a negative number that a `>= 0` check or a bounds check catches immediately. With unsigned arithmetic they produce a number near the type's maximum, which passes every "is it non-negative" check you could write. ## Reason two: Go has no implicit conversions This is the practical reason, and it is specific to Go. There is no automatic widening or signedness coercion: you cannot compare an `int` with a `uint`, add them, or assign one to the other. Both operands of a binary operation must be the same type. So the moment you declare a count as `uint32` in a struct, every interaction with the rest of the language needs a cast: ```go if int(rec.Count) > len(buf) { ... } buf = buf[:int(rec.Count)] ``` Each `int(...)` and `uint32(...)` is unchecked and silently truncating or reinterpreting. You have not removed a class of bug; you have added conversion sites where one can hide. Signed counts keep the code conversion-free and the arithmetic honest. ## When unsigned is exactly right Unsigned types are not discouraged in general — they are discouraged as a way of *documenting* non-negativity. Choose them when the modular behaviour or the extra top bit is the point: - **Bit manipulation.** Masks, flags and shifts. `math/bits` operates on the unsigned types (`bits.OnesCount32`, `bits.LeadingZeros64`) because a sign bit would be meaningless there. - **Hashes and checksums.** `hash/fnv` and `hash/crc32` are defined in terms of wrap-around multiplication and xor; the wrap is the algorithm. - **Fixed-width wire and file formats.** A 16-bit port number, a 32-bit IPv4 address, an unsigned length prefix. `encoding/binary` reads and writes these as `uint16`/`uint32`/`uint64` because that is literally what the bytes mean. - **Interop.** Structures shared with C or with a hardware register map, where the field is defined unsigned. In all of those the type is describing a representation, not asserting a business rule. ## What to say in the interview State the design decision, then the two supports: unsigned wraps at zero so index arithmetic loses its error signal, and Go's lack of implicit conversion turns an unsigned field into a conversion tax at every boundary. Add the one-line demonstration — `a - b` giving 254, or the loop that never ends — because it is concrete and instantly convincing. Then show the counter-case: you *do* reach for `uint32` when writing a protocol header, and that is not a contradiction, because there the width and the wrap are part of the specification you are implementing. If you want the non-negativity enforced, the tool is validation at the boundary — check once when the value enters, and carry an `int` afterwards — not a type whose failure mode is a number two billion times too big.
- What happens if you compare an int with a uint in Go?It does not compile — mismatched types cannot be compared or combined without an explicit conversion. And the conversion is where the real risk sits: `uint(n)` on a negative int produces a huge positive number, and `int(u)` on a large uint can produce a negative one. The compile error is doing you a favour by forcing you to decide which type the comparison should happen in.
- How would you write a safe reverse loop if the index has to be unsigned?Avoid making it unsigned. Keep the index an int and convert only at the point of use, or iterate forward and index backwards: `for i := range b { c := b[len(b)-1-i] }`. If you truly must count down with an unsigned type, loop while `i > 0` and use `i-1` inside the body, so the variable never has to reach -1 to stop.
- Is uint faster than int on 64-bit hardware?No. On the usual 64-bit targets both are one machine word and the arithmetic instructions cost the same. The differences that exist are in division and in comparisons the compiler can prove, not in a general speed advantage. Choose the type for its semantics — modular arithmetic and fixed-width representation for unsigned, ordinary counting for signed — not for imagined performance.
saying these in an interview costs you the question
- Argues a count should be uint because it cannot be negative
- Believes unsigned subtraction clamps at zero
- Expects Go to convert int and uint implicitly for a comparison
- Claims uint is faster than int on 64-bit machines
- Expects i >= 0 to terminate a loop over an unsigned index