skip to content

Byte Order with encoding/binary

encoding/binary writes fixed-width numbers in a byte order you choose, so a header written with BigEndian.PutUint32 can be read by a program that is not written in Go.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

4

What do binary.BigEndian.Uint32 and binary.LittleEndian.Uint32 return for the bytes 00 00 01 00?

level: juniorimportance: must knowfreq 45%

answer

  1. same bytes, two readings
  2. which end holds the big part
  3. one order puts the most significant byte first
  4. 0x00000100 against 0x00010000

basics

~10 s

binary.BigEndian.Uint32 reads the most significant byte first and returns 256. binary.LittleEndian.Uint32 reads the least significant byte first and returns 65536. Same four bytes, opposite orders, different numbers.

solid answer

~40 s

The four bytes `00 00 01 00` are just bytes; the byte order decides which end is the most significant. `binary.BigEndian.Uint32` treats the first byte as the high byte, so it assembles `0x00000100` and returns 256. `binary.LittleEndian.Uint32` treats the first byte as the low byte, so it assembles `0x00010000` and returns 65536. Both are package-level values implementing the `binary.ByteOrder` interface, with `Uint16`/`Uint32`/`Uint64` for decoding and `PutUint16`/`PutUint32`/`PutUint64` for encoding into a caller-supplied slice. Go deliberately gives you no default: you must name the order, because the wire format's specification decides it, not your machine. Big-endian is the traditional network byte order; many device and file formats are little-endian.

code

go · 7 lines
go
b := []byte{0x00, 0x00, 0x01, 0x00}
fmt.Println(binary.BigEndian.Uint32(b))    // 256
fmt.Println(binary.LittleEndian.Uint32(b)) // 65536

out := make([]byte, 4)
binary.BigEndian.PutUint32(out, 256)
fmt.Printf("% x\n", out) // 00 00 01 00

go deeper

for a junior

Be ready to convert a short hex dump into a number both ways and say which reading each byte order gives. Knowing that big-endian puts the most significant byte first is the whole recall here.

for a middle

Explain that BigEndian and LittleEndian are values satisfying the ByteOrder interface, so a decoder can take the order as a parameter, and that the Uint/PutUint methods panic on a short slice rather than returning an error.

for a senior

Show that you treat byte order as a property of the format contract, pinned in tests with golden bytes, and that you can recognise a mismatch from the shape of the wrong value rather than from an error message.

for a principal

Own the rule that every binary format your teams define states its byte order in the spec and that decoders name it explicitly, so no component ever depends on the byte order of the machine it happens to run on.

## The problem byte order solves A number larger than one byte has to be split across several bytes when it travels over a wire or lands in a file. The value 256 is `0x0100` in hex: one byte holding `0x01` and one holding `0x00`. Nothing in the bytes themselves says which of the two comes first. That choice is the **byte order**, or *endianness*. - **Big-endian** puts the most significant byte first: 256 as four bytes is `00 00 01 00`. - **Little-endian** puts the least significant byte first: 256 as four bytes is `00 01 00 00`. So the byte sequence `00 00 01 00` does not have one correct interpretation. Read big-endian it is `0x00000100` = **256**. Read little-endian it is `0x00010000` = **65536**. Both readings are valid; only the format's specification says which one is intended. ## What `encoding/binary` gives you The package exposes two package-level values, `binary.BigEndian` and `binary.LittleEndian`. They are values, not functions, and both satisfy the `binary.ByteOrder` interface: - `Uint16(b []byte) uint16`, `Uint32(b []byte) uint32`, `Uint64(b []byte) uint64` — decode from a byte slice. - `PutUint16(b []byte, v uint16)`, `PutUint32`, `PutUint64` — encode into a byte slice the caller already allocated. Because they are ordinary values, you can pass one around as a `binary.ByteOrder` parameter and let a single decoder handle a format that comes in both flavours: ```go func readHeader(b []byte, order binary.ByteOrder) uint32 { return order.Uint32(b) } ``` ## Bounds, not errors The accessors return no error. `Uint32` requires at least 4 bytes and `Uint64` at least 8; hand it a shorter slice and it **panics with an index out of range**. Likewise `PutUint32` writes into the slice you give it and never grows it — `make([]byte, 4)` first, or use the `AppendUint32` form to append onto an existing slice. This is a deliberate design: these are the fast path, so the checks are the ones the runtime already performs on a slice index. ## Choosing an order You do not choose based on your CPU. You choose based on the format you are speaking: - Internet protocol headers are conventionally big-endian, which is why big-endian is often called *network byte order*. - Plenty of device protocols, sensor frames and on-disk formats are little-endian because the hardware that produced them was. The practical rule is that both ends must name the same order explicitly. Two programs that each "just use the natural one" agree only by luck, and the failure is silent: you do not get an error, you get a plausible-looking wrong number. ## Floats and signed values There is no `PutFloat64`. Convert the bit pattern yourself with `math.Float64bits` and write it as a `uint64`, then read it back with `math.Float64frombits`. Signed integers are handled the same way — write the two's-complement bit pattern as the unsigned type of the same width and convert on the way out (`int32(binary.BigEndian.Uint32(b))`). ## A worked round trip ```go b := []byte{0x00, 0x00, 0x01, 0x00} binary.BigEndian.Uint32(b) // 256 binary.LittleEndian.Uint32(b) // 65536 out := make([]byte, 4) binary.LittleEndian.PutUint32(out, 256) // out is now 00 01 00 00 — the same number, the other order ``` Encoding 256 with one order and decoding with the other gives you 16777216, not 256. That asymmetry — a number that comes back multiplied or divided by a power of 256, or byte-swapped into something absurd — is the fingerprint of a byte-order mistake, and recognising it on sight is most of the value of understanding this.

  • How do you decide which byte order a new wire format should use?
    The specification decides, not your hardware. If the format crosses a network, big-endian is the traditional choice and the least surprising one, since protocol headers are conventionally big-endian. If you are speaking to an existing device or file format, you follow whatever it already uses. The only real requirement is that both ends name the order explicitly instead of assuming.
  • What happens if you call binary.BigEndian.PutUint32 with a 2-byte slice?
    It panics with an index out of range. The Put methods write into the slice you supply and never grow or reallocate it, so you must allocate at least 4 bytes first. If you want growth instead, use the append form — `binary.BigEndian.AppendUint32(buf, v)` returns an extended slice the way `append` does.
  • How do you write a float64 with an explicit byte order?
    There is no PutFloat64. Convert the value to its bit pattern with `math.Float64bits`, write that with `binary.BigEndian.PutUint64`, and reverse it on read with `binary.BigEndian.Uint64` followed by `math.Float64frombits`. If you are already using `binary.Write`, it handles `float64` for you and does exactly this internally.

Writing a date as 01/02 is the same two numbers in either convention; only the agreed reading order tells you whether it is January 2nd or February 1st.

saying these in an interview costs you the question

  • Says Go picks the byte order for you automatically
  • Thinks big-endian and little-endian differ in bit order inside a byte
  • Believes the accessors return an error on a short slice
  • Chooses the order from the local CPU rather than the format spec
  • Assumes reading with the wrong order produces an obvious failure
open as a page

Why does binary.Write refuse a struct containing a string or an int field?

level: middleimportance: should knowfreq 55%

basics

~20 s

binary.Write only encodes fixed-size data, whose byte width is known from the type alone. A string has no fixed width and int has an implementation-defined one, so Write returns an error and binary.Size reports -1 for such a value.

open as a page

A telemetry decoder using binary.Read returns plausible but wrong numbers. How do you tell a byte-order bug from a struct-layout bug?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Hexdump the raw frame and compare it field by field with the specification. Byte-swapped values across every field point to the wrong binary.ByteOrder. A correct first field followed by garbage points to a width or padding mismatch in the Go struct.

open as a page

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

level: middleimportance: nice to knowfreq 28%

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.

open as a page