What is the runtime layout difference between an any value and an io.Writer value?
answer
- empty versus method-bearing
- one of them has nothing to dispatch
- two words wide either way
- only the first word differs
- a table per interface and type pair
basics
~20 sBoth are two words wide. An any value points straight at the concrete type's descriptor; an io.Writer value points at an itab that pairs io.Writer with that concrete type and holds its method addresses. The second word is the data either way.
solid answer
~50 sThe empty interface is laid out as an eface: a pointer to the concrete type's descriptor, plus the data pointer. A method-bearing interface such as `io.Writer` is laid out as an iface: a pointer to an itab, plus the same data pointer. The itab exists per (interface type, concrete type) pair — it records the concrete type and holds the addresses of exactly the methods that interface requires, in a fixed order. That is why `w.Write(p)` is an indirect call through a known slot rather than a lookup by method name, and why converting an `io.Writer` to an `any` only rewrites the first word. Itabs for conversions the compiler can see statically are emitted at build time; the rest are constructed by the runtime on first use and cached, so the construction is paid once.
code
go · 8 linesvar w io.Writer = os.Stdout // iface: (itab for io.Writer/*os.File, os.Stdout)
var a any = os.Stdout // eface: (type descriptor for *os.File, os.Stdout)
n, err := w.Write([]byte("hi")) // indirect call through the itab's Write slot
_, _ = n, err
// a.Write is a compile error: any declares no methods.
fmt.Printf("%T\n", a) // *os.File — the concrete type is still in therego deeper
Know the two spellings you will meet: any (or interface{}) requires no methods, while io.Writer requires a Write method. Both hold a concrete type alongside the value.
Be able to draw both layouts: eface is type descriptor plus data, iface is itab plus data, and the itab is what turns a call into a fixed-slot indirect call instead of a name lookup.
Expect to explain where itabs come from — emitted by the compiler for conversions it can see, built and cached by the runtime otherwise — and why the table is per interface, not per type.
The judgment to own is interface width. Every method you add narrows who can implement the interface and adds a slot each implementation must fill, so a one-method interface is the easiest thing to satisfy and to substitute.
### Two shapes, one size Go has exactly two interface layouts, and both are two machine words: - **eface** — the shape of a value whose static type is the empty interface, written `any` (an alias for `interface{}`, added in Go 1.18). Word one points at the **type descriptor** of the concrete type; word two is the data pointer. - **iface** — the shape of a value whose static type is an interface with at least one method, such as `io.Writer`. Word one points at an **itab**; word two is the data pointer. The data word is identical in both cases. Only the first word carries different information, and the difference is entirely about dispatch. ### What an itab is for An interface with methods needs to answer a question the empty interface never asks: *given this value, where is the code for `Write`?* The itab is the answer, precomputed. An itab belongs to a **pair**: one interface type and one concrete type. The itab for (`io.Writer`, `*os.File`) records the concrete type — so the runtime can still tell you it is a `*os.File` — and holds a small array of function pointers, one per method the interface declares, filled in with `*os.File`'s implementations and laid out in a fixed order the compiler agrees on. A call `w.Write(p)` therefore compiles into: load word one, load the pointer at the slot reserved for `Write`, call it with word two as the receiver. No searching, no hashing, no name comparison at call time. The cost over a direct call is the extra load and an indirect branch. ### Why the empty interface has no itab `any` declares no methods, so there is nothing to fill an itab with and nothing to dispatch. Word one can point directly at the type descriptor. This is also why you cannot call `Write` on a variable of type `any` even when the value inside is a `*os.File`: the variable's callable surface is empty by definition, and the compiler has no method set to check a call against. The value is untouched — only the interface's declared surface is empty. ### Where itabs come from Most conversions are statically visible: the compiler sees `var w io.Writer = f` where `f` is a `*os.File` and can emit the itab for that pair at build time, so the assignment is just two stores. When the pair is only known at run time, the runtime constructs the itab — walking the concrete type's methods to fill the slots — and caches it in a global table keyed by the pair, so subsequent conversions of the same pair reuse it rather than rebuilding it. ### Converting between the two shapes Assigning an `io.Writer` value into an `any` variable does not touch the data word. The itab knows its concrete type, so the runtime can produce the type descriptor for word one directly. The reverse direction — putting an `any` into an `io.Writer` — is the interesting one: the concrete type is known but the pairing may not be, so this is where an itab lookup (and possibly a construction) happens, and where the conversion can fail if the concrete type does not have the methods. ### Practical consequences - **One itab per interface per type.** If `*os.File` is used as both an `io.Writer` and an `io.Closer`, that is two itabs; the data word is the same pointer in both interface values. - **Narrow interfaces are cheap to satisfy.** A one-method interface needs one slot filled, and far more types can fill it. Interface width is an API decision with a mechanical shadow. - **Dispatch is position-based, not name-based.** Go does not do dynamic method lookup by name at call sites; that only appears when you deliberately reach for reflection. - **Equality still works on both shapes.** Both words participate, which is why an interface holding a nil pointer is not the same value as an unset interface. ### The mental model The empty interface says *"here is a value and here is what type it is"*. A method-bearing interface says *"here is a value, and here is the table of the methods this particular interface needs from that value's type"*. Same width, same data word, different first word.
- How many itabs exist if *os.File is used as both an io.Writer and an io.Closer?Two. An itab belongs to one (interface type, concrete type) pair and holds exactly the methods that interface declares, so each interface needs its own. The data word is the same `*os.File` in both interface values — the tables are separate, the value is shared.
- Why can you not call Write on a variable of type any that holds an *os.File?Because `any` declares no methods, so its first word is a plain type descriptor with no method table behind it and the compiler has no method set to check the call against. The concrete value is intact; it is the variable's declared surface that is empty.
- Does converting an io.Writer value into an any value change the data word?No. The data word is carried across unchanged and only the first word is rewritten, from the io.Writer/*os.File itab to the plain `*os.File` type descriptor. The itab records its concrete type, so the descriptor is available without inspecting the value at all.
saying these in an interview costs you the question
- Thinks each interface value carries its own copy of the method table
- Claims method calls resolve by looking up the method name at run time
- Says the empty interface also has an itab
- Believes an itab is per concrete type rather than per interface and type pair
- Treats any and interface{} as two different types