skip to content

What do the two return values of binary.Uvarint mean when the byte count is zero or negative?

level: middleimportance: nice to knowfreq 28%

answer

  1. failure travels in the second return value
  2. no error type in the signature
  3. one failure means read more, the other means give up
  4. the sign of the count separates them
  5. ten is the ceiling for a 64-bit value

basics

~20 s

binary.Uvarint returns the decoded uint64 and the number of bytes consumed. A positive count means success. Zero means the buffer held an incomplete varint and more bytes are needed. A negative count means the value overflowed 64 bits.

solid answer

~50 s

`binary.Uvarint(buf)` returns `(uint64, int)` and reports failure through the int rather than an error. If the count is greater than zero, the value is good and that many bytes were consumed. If it is exactly zero, the buffer ended in the middle of a varint — the caller needs to read more bytes and try again, which is what makes it usable on a partially filled buffer. If it is negative, the encoding described a number wider than 64 bits, and the magnitude of the count tells you how many bytes were read before that was clear. In the failure cases the returned value is zero, so a caller who ignores the count silently turns a truncated or malformed stream into a legitimate-looking 0. The writing side is `binary.PutUvarint`, which returns the bytes written and panics if the destination is too small; size the destination with `binary.MaxVarintLen64`, which is 10.

code

go · 9 lines
go
buf := make([]byte, binary.MaxVarintLen64)
n := binary.PutUvarint(buf, 300)
// n == 2; buf[:2] holds ac 02

v, m := binary.Uvarint(buf[:n])
// v == 300, m == 2

_, short := binary.Uvarint(buf[:1])
// short == 0: the buffer holds an incomplete varint

go deeper

for a junior

Be ready to say a varint spends fewer bytes on small numbers by using seven bits per byte plus a continuation bit, and that the decoder hands back both the value and how many bytes it used.

for a middle

Explain the three cases the byte count encodes — positive is success, zero means the buffer was incomplete, negative means an overflow past 64 bits — and why the value is zero in both failure cases.

for a senior

Show how a frame reader distinguishes 'need more bytes' from 'reject this stream', bounds a decoded length before allocating from it, and knows that the streaming form needs an io.ByteReader rather than a raw connection.

for a principal

Own whether a format uses variable-length integers at all: they shrink payloads of small values but forbid fixed-offset field access and make every decoder sequential, which is a cost the consumers of the format inherit permanently.

## What a varint is A varint is a variable-length encoding for an integer. Instead of always spending eight bytes on a `uint64`, it spends as many bytes as the magnitude needs. Each byte carries seven bits of the number in its low bits; the top bit is a continuation flag saying "another byte follows". Small numbers cost one byte, which is the point: in a format where most values are small counts, lengths or identifiers, the saving over a fixed 64-bit field is large. The cost is at the other end. A `uint64` near its maximum needs ten bytes, two more than the fixed encoding, which is why `binary.MaxVarintLen64` is 10 (`MaxVarintLen32` is 5, `MaxVarintLen16` is 3). ## The decode signature ```go func Uvarint(buf []byte) (uint64, int) ``` There is no `error` here, and that is deliberate — this function sits in tight decode loops and the second return value carries everything a caller needs: - **n > 0** — success. The value is valid and `n` bytes were consumed, so the next varint starts at `buf[n:]`. - **n == 0** — the buffer is too small. It ended part-way through a varint: every byte examined had its continuation bit set and the data ran out. This is not corruption; it is the normal signal when you are decoding out of a partially filled read buffer. Read more bytes and call again from the same offset. - **n < 0** — overflow. The encoding kept going past what fits in 64 bits. `-n` is the number of bytes that were read before that was determined. This *is* a malformed or hostile stream and should be reported, not retried. In both failure cases the returned value is `0`. That is the trap: a caller who writes `v, _ := binary.Uvarint(buf)` cannot distinguish a genuine zero from a truncated buffer or an overflow, and will happily accept 0 as a length, a count or a record identifier. ## The encode side ```go func PutUvarint(buf []byte, x uint64) int ``` It writes into the slice you supply, returns how many bytes it used, and **panics if the buffer is too small**. It does not grow the slice. The safe pattern is a scratch buffer of `binary.MaxVarintLen64` bytes reused across calls, then writing `buf[:n]`. There is also an append form that returns an extended slice, `binary.AppendUvarint(buf, x)`, when you are building up a byte slice rather than filling a fixed one. ## Streaming On an `io.Reader` you cannot hand `Uvarint` a slice of the right size, because you do not know the size until you have read it. That is what `binary.ReadUvarint(r io.ByteReader)` is for: it pulls one byte at a time until the continuation bit clears, and returns `(uint64, error)`. Its error convention mirrors the buffer version — `io.EOF` only if nothing at all was read, `io.ErrUnexpectedEOF` if the stream ended part-way through the varint, and an overflow error for a value wider than 64 bits. Note the parameter is an `io.ByteReader`, not an `io.Reader`, so a plain network connection needs wrapping in a `bufio.Reader` first. ## Signed values `binary.Uvarint` and `binary.PutUvarint` are unsigned only. For signed integers the package provides `binary.Varint` and `binary.PutVarint`, which apply a zig-zag mapping first: small-magnitude negatives map to small unsigned numbers, so -1 costs one byte rather than ten. Encoding a negative number by casting it to `uint64` and calling `PutUvarint` "works" in the sense that it round-trips, but every negative number then costs the maximum ten bytes, which defeats the encoding entirely. ## When to reach for varints at all Varints are worth it when the values are usually small and the field count is large — record lengths, tag numbers, delta-encoded offsets, counters. They are a poor fit for a fixed-layout frame where the receiver wants to index straight to a field at a known offset, because a varint field makes every subsequent offset depend on the data. In that kind of frame a fixed-width field and `binary.BigEndian.Uint32` is both faster and simpler. ## The checklist Check the count, always. Treat `n == 0` as "need more bytes", `n < 0` as "reject this stream", and never let a discarded second return value turn either into the number zero.

  • How do you encode a negative number as a varint without wasting bytes?
    Use `binary.PutVarint` and `binary.Varint` rather than the unsigned pair. They apply a zig-zag mapping so that small negatives become small unsigned values — -1 encodes in one byte. Casting a negative `int64` to `uint64` and calling PutUvarint round-trips correctly but sets the high bits, so every negative value costs the full ten bytes.
  • How do you read a varint straight off a network connection?
    Use `binary.ReadUvarint`, which takes an `io.ByteReader` and consumes exactly the bytes the varint needs. A raw connection is not an io.ByteReader, so wrap it in a `bufio.Reader` first. It returns an error rather than a byte count: io.EOF only when nothing was read, io.ErrUnexpectedEOF when the stream ended mid-varint, and an overflow error past 64 bits.
  • When is a varint the wrong choice for a field?
    When the receiver wants to index into the frame at fixed offsets. A varint field makes every following offset depend on the data, so you must decode sequentially. In a fixed-layout frame a `uint32` read with `binary.BigEndian.Uint32(b[4:8])` is faster and simpler. Varints pay off when values are usually small and there are many of them — lengths, tags, deltas.

saying these in an interview costs you the question

  • Discards the byte count and treats a failed decode as the value zero
  • Reads n == 0 as a successfully decoded zero
  • Assumes a varint always costs eight bytes or fewer
  • Passes a one-byte scratch buffer to PutUvarint and is surprised by a panic
  • Encodes negative numbers with PutUvarint instead of PutVarint