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-nilinterface 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 Callr.Run()) with concrete numbers. - Identifies real-world failure modes (Declaring
var err *CustomErrorat the top of a function that returnserrorand returningerron success) and explains how production systems mitigate them.
Common mistakes
- Treating Go's 16-byte interface layout (
iface/eface) and the typed-nilinterface 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 == nilon ananyparameter when the caller passed a typednilpointer ((*User)(nil))).