Why does binary.Write refuse a struct containing a string or an int field?
answer
- the type must decide the width
- length lives in the value, not the type
- one of the rejected types is a number
- Size hands back a sentinel for anything it cannot encode
- swap to int32 and a byte array
basics
~20 sbinary.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.
solid answer
~50 s`binary.Write` walks the value with reflection and emits each field at a width the type alone determines. That works for `bool`, the sized numeric types (`int8` through `int64`, `uint8` through `uint64`, `float32`, `float64`, the complex types), arrays and structs built from those, and slices of them. A `string` fails because its length is a property of the value, not the type — the package will not silently invent a length prefix or a terminator for you. `int`, `uint` and `uintptr` fail because their width is not fixed by the type either. The fix is to declare the wire shape explicitly: `int32` instead of `int`, `[16]byte` instead of `string`, or write a length prefix yourself and follow it with the bytes. `binary.Size` is the cheap pre-check: it returns the encoded byte count, or -1 for anything not fixed-size.
code
go · 15 linestype frame struct {
Kind uint8
Count int // not fixed-size
Label string // not fixed-size
}
// binary.Size(frame{}) == -1
// binary.Write(w, binary.BigEndian, frame{}) returns an error
type wireFrame struct {
Kind uint8
_ [3]byte // explicit padding, zero-filled on write
Count int32
Label [16]byte
}
// binary.Size(wireFrame{}) == 24go deeper
Be ready to say that binary.Write handles only types with a width you can read off the declaration, and to name string and int as the common rejects. Knowing the fix — int32 and [16]byte — is enough at this level.
Explain why the restriction exists: the encoder has no framing convention to invent for a string and no fixed width to assume for int. Show binary.Size returning -1 as the check, and know that struct fields are packed with no implicit padding.
Demonstrate that you pin the layout in a test by asserting binary.Size against the documented frame width, that you validate a length prefix before allocating from it, and that you move off reflection-driven Read when profiling says the decode is hot.
Own the convention that a wire struct is a separate declared type from the domain type, with widths and padding stated explicitly, so a refactor of internal types can never silently change the bytes another team's device is parsing.
## What "fixed-size" means here `binary.Read` and `binary.Write` take a `binary.ByteOrder` and a value, and move that value between Go memory and a byte stream. Their contract is narrow on purpose: the data must be **a fixed-size value, a slice of fixed-size values, or a pointer to such data**. Fixed-size means the number of bytes is decided by the *type*, before any particular value exists. That set is: - `bool` (one byte) - the explicitly sized integers: `int8`, `int16`, `int32`, `int64`, `uint8`, `uint16`, `uint32`, `uint64` - `float32`, `float64`, `complex64`, `complex128` - arrays of the above, such as `[16]byte` - structs whose fields are all of the above - slices of the above (a slice's *element* width is fixed even though its length is not; the length comes from the slice you pass, not from the stream) Everything else is rejected. The two that trip people up are `string` and `int`. ## Why `string` is rejected A `string`'s byte count belongs to the value, not the type. To encode one, the package would have to choose a framing convention: a fixed-width padded field, a length prefix (and if so, how many bytes and in which byte order?), or a terminator byte. Any choice would be wrong for some wire format. Rather than pick one and make every user of the package live with it, `encoding/binary` refuses and leaves the framing to you. ## Why `int` is rejected `int`, `uint` and `uintptr` do not have a width fixed by the type — the language leaves it to the implementation. A value encoded as an `int` would therefore not have a single, stable byte width for the format to specify, and the encoder would be guessing. So the package requires you to say `int32` or `int64` and make the wire width part of the declaration. ## `binary.Size` is the pre-check `binary.Size(v)` returns how many bytes `binary.Write` would emit for `v`, and **-1** if `v` is not something it can encode. That makes it both a validity test and a layout assertion: ```go if binary.Size(frame{}) != 24 { // the struct no longer matches the documented frame width } ``` A test that asserts `binary.Size` against the number in the protocol specification catches the day someone widens a field from `uint16` to `uint32`, long before a device on the other end starts returning nonsense. ## Fixing a struct that fails ```go type frame struct { Kind uint8 Count int // rejected: no fixed width Label string // rejected: length lives in the value } ``` becomes ```go type wireFrame struct { Kind uint8 _ [3]byte // explicit padding, written as zeros Count int32 Label [16]byte } ``` Now `binary.Size(wireFrame{})` is 24 and `binary.Write` succeeds. Note what the rewrite forced you to decide: how wide `Count` is, how long `Label` is, and where the padding sits. Those were always decisions the format required; the original struct just hid them. For a genuinely variable-length field, the usual shape is a fixed header followed by a body you handle yourself: encode the header with `binary.Write`, then write the bytes; on the way back, read the header, then `io.ReadFull` into a slice sized from the header's length field. Sanity-check that length against a ceiling before allocating — a corrupt or hostile length prefix is otherwise an instruction to allocate whatever it says. ## Struct layout is packed When `binary.Write` encodes a struct it writes the fields back to back with **no implicit alignment padding**. That is not the same as the struct's layout in memory, and it is not the same as what a C compiler produces for the equivalent declaration, which may insert padding to align fields. Blank fields named `_` are the explicit escape hatch: `binary.Write` fills their bytes with zeros and `binary.Read` skips over them, consuming their width. ## Cost Both `Read` and `Write` are driven by reflection, which makes them convenient and comparatively slow. For a hot loop over millions of frames, decode with the `binary.ByteOrder` accessors directly — `binary.BigEndian.Uint32(b[4:8])` — and keep `binary.Read` for the code where clarity matters more than nanoseconds. The package documentation makes exactly this recommendation.
- How would you carry a variable-length name field through binary.Write and binary.Read?Split it. Encode a fixed header that contains a length field — say a `uint16` — with `binary.Write`, then write the name's bytes separately. On the way back, read the header, validate the length against a ceiling you are willing to allocate, then `io.ReadFull` into a slice of that size. The alternative is a fixed-width `[N]byte` field if the format pads names to a constant length.
- When would you decode with binary.BigEndian.Uint32 instead of binary.Read?When the decode is hot. `binary.Read` is reflection-driven and allocates a decoder per call; the ByteOrder accessors are a handful of shifts over a slice you already have. For a frame parsed millions of times, read the whole record into a `[]byte` and pull fields out at known offsets. Reserve `binary.Read` for setup paths and formats where the struct declaration is the clearest documentation of the layout.
- What does binary.Read return when the reader ends part-way through a value?`io.ErrUnexpectedEOF` if it read some but not all of the bytes it needed, and `io.EOF` only when no bytes were available at all. That distinction is what lets a frame loop tell a clean end of stream — stop — from a truncated frame — report corruption. Treating both as end-of-stream silently swallows a truncated final record.
saying these in an interview costs you the question
- Expects binary.Write to length-prefix a string automatically
- Thinks int and int64 are interchangeable on the wire
- Reads binary.Size's -1 as a byte count rather than a failure
- Assumes encoding/binary inserts alignment padding like a C compiler
- Uses binary.Read in a hot loop and calls it zero-cost