skip to content

What is Go's encoding/gob package, and how do you encode and decode a value with it?

level: juniorimportance: must knowfreq 45%

answer

  1. no Marshal function in this package
  2. you wrap a writer, not a byte slice
  3. Decode wants a pointer to write into
  4. capital letters decide what travels

basics

~20 s

encoding/gob is Go's own binary serialization format. Wrap a writer with gob.NewEncoder and call Encode to send a value; wrap a reader with gob.NewDecoder and call Decode with a pointer to receive it. Only exported fields travel.

solid answer

~50 s

`encoding/gob` is the standard library's binary format for moving Go values between Go programs, over a connection or into a file. It is stream-oriented rather than byte-slice oriented: you build an encoder over any `io.Writer` with `gob.NewEncoder(w)` and call `Encode(v)`, and on the other side `gob.NewDecoder(r).Decode(&v)` fills a value you pass by pointer. There is no `Marshal`/`Unmarshal` pair and no tag vocabulary — gob works straight off the Go types through reflection, matching struct fields by name. Two rules bite immediately. Only exported fields are encoded: unexported ones are skipped silently and come back as zero values, and a struct with no exported fields at all is an error. And because the stream describes Go types, in practice only a Go program can read it, so gob is the wrong choice the moment a non-Go consumer is plausible.

code

go · 17 lines
go
type Entry struct {
	Key string
	Value []byte
	hits int // unexported: never encoded, zero after a round trip
}

var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
if err := enc.Encode(Entry{Key: "a", Value: []byte("1"), hits: 7}); err != nil {
	return err
}

var got Entry
if err := gob.NewDecoder(&buf).Decode(&got); err != nil {
	return err
}
// got.Key and got.Value are restored; got.hits is 0

go deeper

for a junior

Be ready to write the four calls from memory: gob.NewEncoder(w), Encode(v), gob.NewDecoder(r), Decode(&v). Know that only exported fields are sent and that Decode needs a pointer.

for a middle

Explain that gob is reflection-driven with no tags and no schema file, that field names are the wire identifiers, and that unexported state must be rebuilt after a decode rather than transmitted.

for a senior

Say when you would reach for gob at all: both ends are Go binaries you build and deploy, such as a shared on-disk cache or an internal transport. Flag it the moment a non-Go consumer becomes plausible.

for a principal

Own the consequence of choosing a Go-only wire format: it quietly forbids a future service in another language from reading the same bytes, so treat it as a decision to revisit whenever the set of consumers might widen.

## What gob is `encoding/gob` is a binary serialization format that ships with the Go standard library. Its job is to move Go values — structs, slices, maps, arrays, numbers, strings, byte slices — between two Go programs, either across a network connection or through a file on disk. It was built for Go's own RPC package, and its design goal is convenience for Go-to-Go traffic, not interoperability with other languages. ## The API Unlike `encoding/json`, gob has no `Marshal`/`Unmarshal` pair handing you a `[]byte`. It is a stream API: - `gob.NewEncoder(w io.Writer) *gob.Encoder` — wrap anything you can write to (a file, a network connection, a `bytes.Buffer`). - `(*gob.Encoder).Encode(v any) error` — write one value onto that stream. - `gob.NewDecoder(r io.Reader) *gob.Decoder` — wrap anything you can read from. - `(*gob.Decoder).Decode(v any) error` — read the next value from the stream into the variable `v` points at. `Decode` must be handed a pointer, because it writes into your variable; passing a non-pointer returns an error rather than quietly decoding into a copy. Passing `nil` is legal and means "read the next value and throw it away". When the stream is exhausted, `Decode` returns `io.EOF`, which is how a read loop terminates. Because it is a stream, an encoder and its decoder are a pair: values come out in the order they went in, and the decoder must read from the beginning. ## Only exported fields travel gob encodes and decodes exported struct fields only. An unexported field is skipped without comment, and after a round trip it holds the zero value for its type. This is not an oversight to work around; it is the rule, and it means any state you keep unexported — a mutex, an open connection, a memoized computation, a logger — has to be rebuilt after decoding rather than transmitted. The degenerate case is an error rather than a silent no-op: encoding a struct type that has no exported fields at all fails, because there would be nothing to send. Two related exclusions are worth knowing early. Functions and channels cannot be encoded; a struct field of `func` or `chan` type is treated exactly like an unexported field and ignored, and attempting to encode such a value at the top level fails outright. ## No tags, no schema file gob is driven by reflection over the Go types themselves. There is no tag syntax for renaming a field, omitting one, or forcing a representation — the Go field *name* is the identifier on the wire, and the two sides agree by using the same names. If a type needs a hand-rolled representation, it can implement the `gob.GobEncoder` interface (`GobEncode() ([]byte, error)`) and `gob.GobDecoder` (`GobDecode([]byte) error`), and gob will use those instead of walking the fields. Equally, there is no separate schema artefact to generate, ship or version — nothing analogous to an IDL file sitting beside your code. The stream carries the type information it needs along with the data. ## Why it stays Go-to-Go That convenience is exactly what pins gob to Go. What the stream describes is a *Go* type: Go field names, Go kinds, Go's notion of a struct, a slice and a map. A reader written in another language would have to model Go's type system to make sense of it, and there is no maintained ecosystem doing that. So the honest rule of thumb is: gob is a fine choice when both ends are Go binaries you build and deploy yourself — an on-disk cache two of your services share, or an internal transport — and a poor one the moment somebody else's language might need to read the bytes. ## The shape of a first program Encode into a buffer, decode back out, and inspect what survived. The one surprise for a newcomer is usually the unexported field that comes back zero, and the second is that `Decode` insisted on a pointer. Both follow from the rules above.

  • Does gob need struct tags the way encoding/json does?
    No — gob has no tag vocabulary at all. It maps by Go field name, so the field names are what the two programs must agree on, and there is no way to give a field a different name on the wire. If a type needs a custom representation, implement `gob.GobEncoder` and `gob.GobDecoder` on it and gob will call those instead of walking the fields.
  • What happens if you pass a value rather than a pointer to Decode?
    It returns an error. `Decode` has to write into your variable, so it requires a pointer to the destination; the only other accepted argument is `nil`, which reads the next value and discards it. It does not silently decode into a copy, which is a mercy — that failure would be invisible.
  • Can a struct field of channel or function type be encoded?
    No. Functions and channels are not transmissible, and a struct field of `func` or `chan` type is treated exactly like an unexported field: skipped, with no error. Trying to encode such a value at the top level fails instead. Anything of that kind has to be reconstructed after the decode.

A gob stream is a parcel that carries its own assembly instructions inside the box — but the instructions are written in Go, so only a Go program can follow them.

saying these in an interview costs you the question

  • Says gob has Marshal and Unmarshal like encoding/json
  • Expects unexported fields to round-trip
  • Assumes another language can read a gob stream
  • Passes a value instead of a pointer to Decode
  • Thinks struct tags rename fields on a gob wire