skip to content

eface, iface and the itab

What an interface value physically is: a pair of pointers, where the first is either a plain type descriptor or an itab carrying the method table for that concrete type. Interviewers use it to get a concrete answer about dispatch cost and about why storing a struct in an interface can allocate.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

4

A wiring function returns a nil *fileSink as a Sink interface, the caller's nil guard passes, and the first call panics — why?

level: seniorimportance: must knowfreq 58%

answer

  1. two words, only one of them nil
  2. what does the type word hold here
  3. nil means both words unset
  4. the guard never tested the pointer
  5. print %T beside the comparison

basics

~20 s

The interface's type word is set to the Sink and *fileSink pairing while its data word is nil, so the value is not equal to nil. The guard passes, the call dispatches, and the method dereferences a nil receiver.

solid answer

~50 s

An interface value is a pair — a type word and a data word — and it compares equal to `nil` only when **both** are unset. Returning a nil `*fileSink` from a function whose result type is `Sink` performs a conversion: the type word gets the itab for the (`Sink`, `*fileSink`) pairing and the data word gets the nil pointer. The result is a non-nil interface wrapping a nil pointer, so `if s != nil` is true. The call then dispatches through the itab into `(*fileSink).Write` with a nil receiver, which is legal right up to the moment the method touches a field — then it panics with a nil pointer dereference. The one-line diagnosis is `fmt.Printf("%T %v\n", s, s == nil)`, which prints `*fileSink false`. The fix is in the constructor, not the guard: never let a typed nil pointer become the interface value.

code

go · 16 lines
go
type Sink interface{ Write(p []byte) error }

type fileSink struct{ f *os.File }

func (s *fileSink) Write(p []byte) error {
	_, err := s.f.Write(p) // panics if s is nil
	return err
}

func openSink(path string) Sink {
	var s *fileSink // nil
	if path != "" {
		s = &fileSink{f: os.Stdout}
	}
	return s // converted to Sink: type word set, data word nil
}

go deeper

for a junior

Remember the rule: an interface variable equals nil only when nothing has been assigned to it. Putting a nil pointer inside one does not make the interface itself nil.

for a middle

Explain it with the two words — type word set, data word nil — and point at the exact line that performs the conversion, which is the return statement whose result type is the interface.

for a senior

Diagnose from the symptoms: a nil guard that passes and a dereference panic one frame later. Print %T next to the comparison, then repair the constructor rather than hardening the call sites.

for a principal

Set the convention. Decide once for the codebase how optional dependencies are handed out — concrete return types, an untyped nil, or a do-nothing implementation — so nobody has to remember this rule at every call site.

### The symptom Someone new to the codebase reads a wiring function that assembles services behind interfaces. Some dependencies are optional — a sink that only exists when a path is configured, say — so the code guards every use with a nil check. The guard passes and the very next line panics with `invalid memory address or nil pointer dereference`. From the outside this looks like Go contradicting itself: the value is not nil, yet dereferencing it fails. ### The mechanism An interface value is two words: - **the type word** — for a method-bearing interface like `Sink`, a pointer to the itab pairing `Sink` with the concrete type; - **the data word** — the concrete value, or a pointer to it. `s == nil` is true only when **both** words are zero. That is the whole rule, and everything else follows. Now look at the conversion. A function declared `func openSink(path string) Sink` returning a `*fileSink` variable performs an implicit conversion at the `return`. The conversion sets the type word to the (`Sink`, `*fileSink`) itab — it must, because the caller has to be able to dispatch — and copies the pointer, nil or not, into the data word. One word set, one word nil. The interface is not nil. The caller's `if s != nil` was never testing the pointer. It was testing whether anything at all had been assigned, and something had. ### Why the panic comes one line later, not at the guard Dispatch through the itab does not check the receiver. A nil `*fileSink` is a perfectly legal receiver: a pointer-receiver method can run on nil and behave sensibly, and some types deliberately do exactly that. The panic happens only when the method body dereferences the receiver to reach a field. That is why this bug can sit in a codebase for months and then appear the day someone adds a field access to a method that previously only returned a constant. ### Diagnosing it in one line Print the dynamic type next to the comparison: ```go fmt.Printf("%T %v\n", s, s == nil) // *fileSink false ``` A concrete type printed for a value you believe is nil, next to a `false`, *is* the diagnosis. `%v` alone is misleading here: it prints `<nil>`, because formatting follows the data word, which really is nil. The pair of outputs is what separates "unset interface" from "set interface holding nothing". ### The fixes, in order of preference 1. **Return an untyped nil on the path that has nothing to build.** `return nil` in a function returning `Sink` leaves both words unset, so the caller's guard means what the caller thinks it means. 2. **Return the concrete type from the constructor.** `func openSink(path string) (*fileSink, error)` never performs the conversion; the caller converts explicitly and can decide what to do with a nil pointer. Returning concrete types and accepting interfaces avoids the whole class of bug. 3. **Return a do-nothing implementation instead of nil.** A `nopSink` whose methods discard their input removes the optionality from the call sites entirely: nobody has to check, so nobody can check wrongly. What is *not* a fix: more nil checks at call sites, a nil check inside every method, or a comment explaining the trap. Those leave the trap armed for the next person. One related detail worth knowing: declaring the local as `var s Sink` rather than `var s *fileSink` also fixes it, because assigning nothing to an interface variable leaves both words unset. The bug lives in the conversion, not in the `return` keyword. ### Why Go behaves this way It is not an oversight. The type word has to be set for the interface value to be usable at all, and the runtime cannot reasonably decide that a nil pointer means "pretend nothing was assigned" — a nil receiver is a valid, sometimes intentional, value. The alternative would make `s == nil` depend on the concrete type's internals rather than on whether the interface holds anything. ### The mental model A labelled empty box. The label says `*fileSink`, so something *did* arrive; the box has nothing in it. A receiving desk that only checks whether a parcel arrived waves it through, and the trouble starts when someone opens the box.

  • Would the panic still happen if the method never touched its receiver?
    No. A nil pointer is a legal receiver, and a method that does not dereference it runs fine — some types rely on that deliberately. It is why this defect hides for a long time and then surfaces the day someone adds a field access to the method.
  • How do you confirm it at run time in one line?
    Print the dynamic type beside the comparison: `fmt.Printf("%T %v\n", s, s == nil)` gives `*fileSink false` for a value you believed was nil. `%v` on its own prints `<nil>` and misleads you, because formatting follows the data word.
  • What is the best fix in the constructor?
    Do not let a typed nil pointer become the returned value. Return an untyped `nil` on the path with nothing to build, or return the concrete `*fileSink` type and let the caller convert, or hand back a do-nothing implementation so call sites never need a nil check at all.
  • Why does declaring the local variable as Sink instead of *fileSink also fix it?
    Because assigning nothing to an interface variable leaves both of its words unset, so it is a genuine nil interface. Assigning a nil `*fileSink` to it would set the type word again. The defect is in the conversion, so removing the conversion removes it.

An empty box with a shipping label on it. The label says fileSink, so it is not nothing, and the receiving desk — which only checks whether a parcel arrived — waves it straight through.

saying these in an interview costs you the question

  • Says the interface is nil because the pointer inside it is nil
  • Blames the method for not handling a nil receiver
  • Adds more nil checks at call sites instead of fixing the conversion
  • Thinks assigning a nil pointer to an interface leaves the interface unset
  • Claims a value receiver would have made the guard work
  • Reads %v printing <nil> as proof the interface is nil
open as a page

When an *os.File is assigned to an io.Writer variable, what does that interface variable hold?

level: juniorimportance: should knowfreq 45%

basics

~20 s

A pair of words: the dynamic type *os.File, plus a data word pointing at the file. The variable is not a copy of the file, and it carries the concrete type along, which is why %T prints *os.File.

open as a page

What is the runtime layout difference between an any value and an io.Writer value?

level: middleimportance: should knowfreq 40%

basics

~20 s

Both 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.

open as a page

Why does storing an int in an any variable allocate when storing a *bytes.Buffer does not?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

An interface's second word is one pointer the garbage collector scans, so a pointer-shaped value like *bytes.Buffer goes in directly. An int is not a pointer, so the conversion copies the value somewhere addressable — often the heap.

open as a page