TRT
Sign in
easyFoundationsdebugging

Explain how interfaces are represented in the Go runtime (`iface` vs `eface` and the `itab` table), why returning a `nil` concrete pointer as `error` breaks.

Trace the 16-byte `(tab, data)` struct, explain `itab` hash-table lookup on interface conversion, and contrast `[]T`'s contiguous memory layout against `[]any`'s array of 16-byte `eface` structs.

TRT practice prompt — not a verified question from a named employer.

The Runtime Theory Team2 min read

A strong answer

In Go, an interface value (error, io.Reader, any) is a 16-byte two-word struct in memory: Word 1 is the type descriptor / method table pointer (tab *itab for iface with methods, or _type *_type for empty interface eface / any), and Word 2 is the data pointer (data unsafe.Pointer). Crucially, an interface value is equal to nil if and only if BOTH words are nil: (tab == nil, data == nil)! When validate(10) returns p of concrete type *ValidationError with value nil through a return type of error, Go boxes p into the 16-byte error interface struct as (tab: *itab_for_*ValidationError, data: 0x0)! Because the first word (tab) is non-nil, err != nil evaluates to true, and calling a method on err dereferences data = 0x0! Always declare the error variable as var err error (or return literal nil directly) so both words stay (nil, nil).

A 16-byte Go interface value (tab, data) compares equal to nil if and only if tab == nil (which implies data == nil); boxing a typed concrete pointer (*T)(nil) into an interface sets tab = &itab(*T) and data = nil, making iface != nil evaluate to True! Because Go's == comparison between an interface (tab, data) and untyped nil compiles to the single check tab != nil, any non-nil type descriptor tab causes iface != nil to return true even when the payload pointer data is 0x0.

Method dispatch through an interface (r.Read(buf)) looks up the function pointer at a fixed offset in r.tab.fun[i] (~2–3 ns indirect call), while boxing a non-pointer value larger than a pointer (or whose address escapes) into an interface triggers a heap allocation (runtime.convT). In production systems (Go runtime/runtime2.go (struct iface, struct eface), global itabTable cache (which caches (interface_type, concrete_type) method tables in a lock-free hash table), and standard library error / io.Reader contracts.), engineers monitor these exact trade-offs to keep p99 latency predictable and prevent resource exhaustion under peak load.

What the interviewer is listening for

  • Traces how Go's 16-byte interface layout (iface / eface) and the typed-nil interface bug operates in memory step by step rather than giving a vague textbook definition.
  • States the core correctness invariant and explains why edge cases do not violate it.
  • Quantifies time, space, and hardware costs (Direct Method Call w.Run(), Interface Call r.Run()) with concrete numbers.
  • Identifies real-world failure modes (Declaring var err *CustomError at the top of a function that returns error and returning err on success) and explains how production systems mitigate them.

Common mistakes

  • Treating Go's 16-byte interface layout (iface / eface) and the typed-nil interface bug as a black box without explaining the underlying memory or state transitions.
  • Ignoring worst-case tail latency or memory overhead (Passing a large struct by value into an any / interface parameter inside a hot loop (log(bigStruct))).
  • Forgetting boundary conditions and error recovery (Checking if x == nil on an any parameter when the caller passed a typed nil pointer ((*User)(nil))).

This answer walks

Practice follow-ups

  1. 01Why does passing `[]int` to a function expecting `[]any` fail to compile in Go? (Because `[]int` is a contiguous array of 8-byte ints, while `[]any` is a contiguous array of 16-byte `eface`.
  2. 02How does Go 1.18+ Generics (`func Sum[T int64 | float64](xs []T) T`) avoid the boxing allocation and indirect-call overhead of `interface{}`?

Not started

Sign in to save your learning progress.

Sign in to save