skip to content

What does Go's per-type runtime descriptor contain, and what reads it at run time?

level: middleimportance: should knowfreq 34%

answer

  1. one copy per type, in read-only data
  2. values carry none of it, interfaces do
  3. size, alignment, kind, name, equality
  4. which words hold pointers
  5. precise scanning needs this map

basics

~20 s

Each type gets one shared descriptor in read-only data holding its size, alignment, kind, name and an equality helper, plus a bitmap marking which words of a value hold pointers. The collector reads the bitmap; interfaces, assertions, printing and reflection read the rest.

solid answer

~50 s

The compiler emits one descriptor per type and the linker places a single copy in the binary's read-only data. It records the type's size, its alignment, a kind tag saying whether it is a struct, slice, map, pointer and so on, its name as a string, a hash and an equality function used for map keys and `==`, and — the part the runtime depends on most — a bitmap describing which machine words inside a value of that type contain pointers. The garbage collector uses that bitmap to scan an object precisely: it follows exactly the pointer words and ignores the rest, and a type with no pointers at all is never scanned. Everything else that needs runtime type identity reads the same descriptor: an interface value points at it, a type assertion compares descriptor identity, `%T` and `%v` print from it, and reflection is essentially an API over it.

code

go · 9 lines
go
type Point struct {
	X, Y int
}

type Node struct {
	Value int
	Next  *Node
	Name  string
}

go deeper

for a junior

Know that Go keeps information about each type in the compiled program — its size, its name and where its pointers are — and that this is what makes printing a value's type and using reflection possible.

for a middle

Be ready to list what the descriptor records and to explain the pointer bitmap: the collector follows exactly the marked words, so a pointer-free type is never scanned.

for a senior

Show the practical consequence: layout decisions change the bitmap and therefore scanning work, and everything the runtime must describe is data occupying space in the shipped binary.

for a principal

Frame it as a budget you set — how much runtime describability the codebase buys, since type metadata is simultaneously what makes generic tooling possible and what inflates the artifact.

## One descriptor per type, shared by every value Go has no per-object type header the way some managed runtimes do. Instead, for each type that needs a runtime identity the compiler emits a **type descriptor**: a single read-only structure in the binary. A value of that type carries no pointer to it; only things that erase static type information — an interface value, a reflection object, the arguments to a function taking `...any` — carry the descriptor along beside the data. ## What the descriptor records - **Size** in bytes, so the runtime can copy, allocate and compare values of the type without knowing it statically. - **Alignment** requirements, for allocation and for laying the type out inside another. - **Kind** — a small tag distinguishing struct from slice from map from pointer from interface, which is what reflection reports when it tells you the general shape of a value. - **A name string**, which is what `%T` prints and what a panic message shows when a type assertion fails. - **A hash and an equality function**, used when values of the type are map keys or are compared with `==` through an interface. - **A pointer bitmap** describing which words of the value hold pointers. - **Links to related types**, such as the pointer type `*T` for a type `T`, and for types with methods a list of their methods. The exact field layout is an internal detail that changes between releases; what is stable and worth knowing is the *set of facts* the runtime needs and the fact that they live in one shared, read-only place. ## The pointer bitmap and precise collection Go's collector is precise, not conservative: it does not guess that a word might be a pointer because it looks like an address. When it scans a heap object it needs to know exactly which words to follow, and the type descriptor's bitmap is where that knowledge comes from. Consider: - `struct { X, Y int }` — no pointer words at all. Objects of this type are allocated as pointer-free and the collector never scans them. That is not just a small saving; whole graphs of such data cost the collector nothing to trace. - `struct { Value int; Next *Node; Name string }` — the `Next` field is a pointer word, and `Name` is a two-word string header whose first word is a pointer to the bytes. The bitmap marks those words and only those. The `int` and the string's length are skipped. Two refinements follow from this. First, the descriptor also records how far into the value pointers can occur, so a struct with a large pointer-free tail is only scanned up to its last pointer. Second, for very large types a flat bitmap would itself be large, so the compiler emits a compact encoded form — a small program the runtime runs to reproduce the bitmap — instead. This is why field ordering has a real effect on collection cost, and why converting a pointer-heavy structure into indices into a flat slice can shrink scanning work dramatically: you are editing the bitmap. ## Who else reads the descriptor - **Interface values.** Storing a concrete value in an interface pairs the data with its descriptor. Without the descriptor there would be nothing to compare or print. - **Type assertions and type switches.** These compare descriptor identity, which is why two identical-looking struct types declared in different packages are distinct. - **The `fmt` package.** `%T` prints the name from the descriptor, and formatting an arbitrary value walks the type through reflection. - **Maps and equality.** A map with an interface or struct key calls the descriptor's hash and equality helpers. - **Reflection.** The reflection API is, essentially, a typed reader over descriptors. ## The consequence for binaries Because descriptors are data, everything the runtime must be able to describe costs space in the shipped file: the descriptor itself, the name strings, the method lists, and the tables produced when a type is converted to an interface. That is the direct link between this structure and the size of a Go artifact. The linker drops descriptors for types that nothing can reach — but a type that reaches an interface, a `fmt` call or reflection is reachable, and its metadata stays. ## What interviewers are checking That you know runtime type information in Go is *emitted data*, not something derived from an object header at run time; that you can name the pointer bitmap and say the collector is precise because of it; and that you connect the same structure to interfaces, printing, map keys and reflection rather than treating each as a separate mechanism.

  • Why is a struct of only integers cheaper for the collector than one containing a string?
    Its descriptor's bitmap has no pointer words, so objects of that type are never scanned at all. A string field contributes a pointer word to the bytes, so the collector must follow it and keep the backing array alive.
  • Two packages declare structurally identical struct types. Why does a type assertion between them fail?
    Because assertions compare descriptor identity, not shape. Each declaration produces its own descriptor with its own name, so they are different types at run time even though their fields match exactly.
  • Does reordering a struct's fields change anything the runtime sees?
    Yes. Field order changes the layout, so it changes the size after padding and the pointer bitmap, including how far into the value the collector must scan. Grouping pointer fields together can shorten the scanned prefix.

saying these in an interview costs you the question

  • Says every Go object carries a type header like a managed runtime
  • Claims the collector scans every word looking for addresses
  • Thinks type information is erased at compile time
  • Believes each value stores its own copy of the descriptor
  • Says field order cannot affect memory or collection cost