Deep Engineering
Intermediate·Published·30 MIN

Interfaces in Go: two words, a nil that is not nil, and a call four times dearer

An interview on interfaces climbs a ladder: what sits in the variable — why nil is sometimes not nil — when boxing allocates — what a call through an interface costs — how a type assertion works. The lesson climbs all of it, and everything follows from one fact: there are two words, not one.

Full technical treatment

TL;DR

A type implements an interface silently. No implements, no list of interfaces on the type: having the required methods is enough. Hence the main design consequence — an interface belongs to whoever consumes it, is declared small, and lives next to the need rather than next to the implementation.

And hence the two places where an interface behaves unlike expected. First: a method with a pointer receiver is not in the method set of the value, and the compiler says so in as many words — Point does not implement Stringer (method String has pointer receiver). Second: an interface variable carries a pair — a dynamic type and a dynamic value — and equals nil only when both halves are empty. Put a nil pointer into it: there is no value, but the type is known, and err == nil gives false. This is Go's most famous trap.

Beyond that come the layout and the numbers. In the current implementation that pair sits in memory as two words: measured, any and Stringer take 16 bytes against 8 for a pointer. The second word is a pointer, so boxing a struct allocates (1 allocation) while boxing a ready pointer does not (0); small integers are 0 too, the runtime keeps a ready array of 0 to 255. The first word is a method table, so the compiler cannot see which code will run and does not inline the call: on an empty method it came out at 11,461 ns against 2,804, a factor of 4.1. A type assertion and a type switch are a type-pointer comparison rather than a walk over a structure; the gap between them is small (4,521–5,023 ns against 3,661–4,128) and does not decide the form.

Where to start
Before this lesson it is enough to understand
  • types can have methods, and a method is called as x.M();
  • a function can take a value it knows nothing about except what that value can do;
  • Go has pointers: &x is the address of a value, and through it the value can be changed.
You do not need to know in advance
  • what a method set is, what a dynamic type is, what boxing a value into an interface means;
  • any, itab, type assertions, type switch, the typed nil.

What is really being asked

The ladder is almost always this one:

  1. "How is an interface built inside?" — testing whether you say "two words" or stop at the word "contract".
  2. "Why did err != nil fire when nil was returned?" — the famous trap; asked more often than everything else combined.
  3. "When does putting a value into an interface allocate?" — testing whether you understand that the second word is a pointer.
  4. "Is a call through an interface dearer than a direct one? By how much?" — a question about honesty: "negligible" and "many times" are equally bad without a number.
  5. "How does x.(T) differ from switch x := v.(type)?" — testing whether you know both forms do the same thing.
  6. "Why does my type not implement the interface?" (with code) — about method sets and pointer receivers.

The lesson climbs that ladder from the bottom: first what an interface means, then what follows from it, and only at the end how it is laid out in memory and what it costs.

Base: implementation without a declaration

An interface in Go is a list of methods with a name on it. Stringer says nothing about what a value is made of or where it came from; it says one thing: "this can do String() string". A function declaring a Stringer parameter therefore demands not a particular type but an ability.

Then comes the decision most languages do not make: a type implements an interface silently. No implements, no list of interfaces on the type — the compiler checks for itself whether the methods are there, and if they are, the value fits. Nothing has to declare the intent, and there is nowhere to declare it.

language contractA language guarantee: implicit implementation and the method-set rules are set by the specification. Two words, itab and the cost of a call are implementation rather than language, and are treated separately.

bench/gointerface/methodsets.sh builds and runs a program in which Point never mentions Stringer:

GO
type Stringer interface{ String() string }
 
type Point struct{ X, Y int }
 
func (p Point) String() string { return fmt.Sprintf("(%d,%d)", p.X, p.Y) }
 
var s Stringer = Point{1, 2}
assigned: (1,2)
--- exit code: 0

It built and it printed. Two consequences follow, and interviewers value them above knowing how an interface is laid out in memory.

An interface belongs to whoever CONSUMES it, not to whoever implements it. You can declare Stringer in your own package — and Point from somebody else's library will implement it while knowing nothing about your existence. In a language with explicit implementation that is impossible: to make a type fit an interface you have to change the type.

Hence "accept interfaces, return structs". A function has no business demanding an interface declared somewhere else: it declares its own, minimal one — exactly the methods it needs. Everything that has them will fit.

The practical corollary: interfaces in Go are small. io.Reader is one method, io.Writer is one. An interface with ten methods almost always means it was declared from the implementation's side rather than from the need's.

That is already enough to answer the basic interview question: an interface is a requirement on behaviour, and a type meets it simply because the methods are there. Everything below is about where that silence has its boundary, what an interface variable actually holds, and why it can differ from nil exactly when "there is a nil inside".

Mechanism 1: method sets — where the silence ends

Silent implementation has exactly one boundary, and everybody trips over it. A method with a pointer receiver is not in the method set of the value.

The same script tries to build a program where the receiver is a pointer and the value is assigned:

GO
func (p *Point) String() string { return "point" }
 
var byValue Stringer = Point{1, 2}     // an error
var byPointer Stringer = &Point{1, 2}  // legal
bad.go:10:25: cannot use Point{…} (value of struct type Point) as Stringer value in variable declaration: Point does not implement Stringer (method String has pointer receiver)
--- go build exit code: 1

The error is worth reading out loud in full: it names the reason itself.

The reason is not syntax. A value put into an interface is a copy, and a copy has no address. A method with a pointer receiver could not change the original; silently changing the copy would be worse than a compile error, so the compiler refuses up front.

The other direction is allowed: a pointer has an address, and dereferencing it for a value method always works. Hence a table worth keeping in mind:

method declared asin the method set of Tin the method set of *T
func (t T) M()yesyes
func (t *T) M()noyes

The practical rule: one kind of receiver for the whole type. Mixing is legal, but then the method set depends on what exactly was assigned — and the error turns up somewhere other than where it was made.

Mechanism 2: the dynamic type and the dynamic value

Now the model everything else follows from — and it is not about the runtime. An interface variable carries two things: which concrete type is in it right now, and which value of that type.

a variable of interface type stores a pair: the concrete value assigned to the variable, and that value's type descriptor

The Laws of Reflection

Two things, not one — and this is not an implementation detail but observable semantics:

GO
var s Stringer          // type: none,   value: none
s = Point{1, 2}         // type: Point,  value: {1, 2}
s = &Point{1, 2}        // type: *Point, value: an address

Three separately-asked things follow immediately.

A type assertion hands the concrete type back. s.(Point) asks not "is it similar" but "is the dynamic type exactly Point?". Which is why *Point will not satisfy Point — those are different dynamic types.

Comparing interfaces compares the whole pair. They are equal if both the type and the value match. Hence, too, the panic when comparing interfaces holding an incomparable dynamic type: there is nothing to compare a slice with.

And most importantly — an interface is empty only when BOTH halves are. That is the next mechanism, and it is the most-asked question in Go interviews.

Mechanism 3: the nil that is not nil

This is the single most common Go interview question, and it must be answered with a mechanism rather than a phrase.

There is one rule, and it is written in the language FAQ:

An interface value is nil only if the V and T are both unset

Go FAQ — Why is my nil error value not equal to nil?

Now look at what happens to the two words in each of four cases — switch between them:

The whole trap is visible in switching between the first and second case: the second word is empty in both, and they differ only in the first. That is exactly why

GO
var p *NotFound      // a nil pointer
var err error = p    // but the interface is no longer empty: the type is known
fmt.Println(err == nil)   // false

gives false. The value inside is a nil pointer, but the word holding the type is filled, and the rule demands that both be empty.

Where this fires in real code. Always in one place — when a function is declared as returning error while inside it returns a concrete error type:

GO
func do() error {
    var e *MyErr          // nil
    if bad() {
        e = &MyErr{}
    }
    return e              // TRAP: even when e == nil, error != nil
}

The caller writes if err != nil — and gets an "error" that does not exist.

There are three cures, and all three are worth naming:

  1. Declare the variable with the interface type, not the concrete one: var err error — then, with no error, both words really are empty.
  2. Return nil explicitly in the success branch: return nil, not return e.
  3. Do not keep concrete error types in intermediate variables — that is the root cause, and the first two points treat its symptoms.

A rule worth memorising: a concrete type does not quietly survive assignment into an interface. Once a value is put into an interface, its type stays there — even when the value itself is absent.

Mechanism 4: a type assertion is a pointer comparison

"How does x.(T) differ from a type switch" is asked expecting an answer about speed. The right answer starts with the fact that both forms do the same thing: they compare the pointer to the type descriptor in the first word against one known at compile time. This is not a walk over a structure and not reflection — it is a comparison of two addresses.

Measured on equal work — in the same runs, on the same machine as every other number in this lesson:

formtime
if x, ok := v.(int); ok4,521–5,023 ns
switch x := v.(type)3,661–4,128 ns

The run ranges do not overlap and the ratio is 0.81 — but that is a fifth of a difference on an empty operation, and it does not decide the form. Nor should that ratio be carried over to your own code: it is about these two loops on this machine. The form is chosen by the number of types: one branch is written as an assertion, several as a switch.

What really does need knowing about these forms:

  • The one-result form panics, the two-result one does not: v.(int) against v, ok := x.(int). In code that does not control where the value came from, always the second.
  • The specification requires x not to be nil: x.(T) asserts that x is not nil and that the value stored in x is of type T. An assertion on a clean nil interface never succeeds.
  • case nil in a type switch catches exactly the clean nil — the one where both words are empty. A typed nil lands in the branch for its own type, and that is one more place where the nil-that-is-not-nil trap bites.

Mechanism 5: how this gets used

The last mechanism is not about the layout but about what all of the above turns into in real code. Three rules, each following from what has already been said.

Interfaces are declared small and where they are consumed. That follows directly from the Base: since implementation is implicit, an interface need not live next to the type. A function declares exactly the methods it needs — and everything that has them fits. A sign of trouble: an eight-method interface declared next to its single implementation. Such an interface abstracts nothing; it restates a struct.

Accept interfaces, return structs. An interface parameter widens the range of what can be passed in; an interface result, conversely, narrows what the caller can do with it and hides methods the concrete type actually has. There is one accepted exception: returning an interface makes sense when the concrete type genuinely varies — as with error.

At boundaries, watch the type-and-value pair. The typed-nil trap lives exactly where an interface crosses a package boundary: inside, the function works with *MyErr; outward it hands back an error. The rule is simple — at the boundary declare the variable with the interface type, var err error, and return nil explicitly.

Hence, too, the answer to the frequent "why have an interface at all if there is one implementation". For the same reason io.Writer exists: so the caller can substitute their own. If there is nobody to substitute and nobody planned to — the interface is not needed, and its absence is not a design flaw.

Deeper: how it is built inside

Everything above described what an interface means, and it rests on the specification. Now how it is made — and here the claims change kind: not rules of the language but the current implementation.

implementation detail · Go 1.24The layout of an interface variable is not in the specification: the spec speaks of a pair of «type and value», not of how many machine words that is or what sits in each. Below is the current implementation.

The type-and-value pair lives in memory as two machine words. That is not a promise of the language but how it is made today — and it is measurable. What an interface variable takes:

size
any16 bytes
Stringer16 bytes
*Point8 bytes
Point (a struct of two ints)16 bytes

Sixteen bytes where a pointer needs eight — that is two machine words. What lies in them is said plainly on the Go blog:

a variable of interface type stores a pair: the concrete value assigned to the variable, and that value's type descriptor

The Laws of Reflection

One refinement about the first word: for an empty interface (any) it is just a type descriptor, while for an interface with methods it is a method table (itab), where the addresses of the implementations sit beside the type. That difference matters for the cost of a call, below.

One consequence per word from here on: the second explains allocations, the first the cost of a call.

The second word is a pointer, so boxing allocates

The second word holds not the value but its address. Hence a simple rule: if what is being boxed does not fit in a pointer, it has to be put somewhere — and that is a heap allocation.

Measured (bench/gointerface/internals.go):

what goes into anyallocations
an int variable holding 420
an int variable holding 10001
a Point struct variable1
a ready *Point pointer0

Three rows are explained by one sentence, the fourth separately.

A pointer goes in as is: there is nothing to copy, zero allocations. A struct has to be placed on the heap, because only an address fits in the second word. But 42 against 1000 is a separate fact worth knowing: the runtime keeps a ready array of small integers, and for numbers from 0 to 255 an address is taken from there rather than allocated afresh.

That, incidentally, is a good answer to "how would you measure it": take the same code with a constant instead of a variable and the allocations become zero in both cases — the compiler boxes a constant at compile time. The first draft of this measurement got it wrong in exactly that way.

The practical point they want to hear: the cost of an interface is not "the interface" but the fact that the boxed value does not fit in a word. So in hot code interfaces over pointers are free memory-wise, and interfaces over structs are not.

The first word is a table, so a call through it is dearer

measured observationbench/gointerface, go1.24.7 linux/amd64, two cores. The numbers were taken on one machine and on an empty method: this is an upper bound on the difference, not a property of interfaces in general.

A call through the table is dearer than a direct one. Here the cause must not be muddled, or the answer sounds like superstition.

What is dear is not looking the method up in the table — that is pennies. What is dear is that the compiler stops seeing which code will run: a direct call it inlines whole and optimises together with the surrounding code, while a call through an interface has to go through the method table and stays a call.

Measured over 4,096 shapes of two concrete types — in one run on one machine, and the ratio holds for exactly that run:

time
direct call2,804 ns
through an interface11,461 ns
ratio4.1

And right here — how such a measurement must be done. The first draft compared a call on a concrete-typed variable with a call on an interface-typed one and got 0.87 — "the interface is faster". That was not a finding but an admission that the measurement was worthless: for a package-level variable of interface type the compiler knows the single concrete type, devirtualises the call and inlines it. To keep a call a call you need two different concrete types in a slice and data from memory rather than from a literal.

This is worth saying out loud in an interview: "how did you measure it" almost always follows, and "I used two types so the compiler could not devirtualise" is the answer that separates someone who measured from someone who repeated.

What follows for code. Nothing like "do not use interfaces". Four times is four times on an empty method in a tight loop; as soon as there is real work inside, the call's share falls to nothing. The rule is this: an interface at a boundary is normal, an interface inside a hot million-iteration loop is something to measure.

How to answer in an interview

Short answer: an interface is a list of methods, and a type implements it silently, simply because it has those methods. Hence the two things asked next: a method with a pointer receiver is not in the method set of the value, and the interface variable itself carries a pair — a dynamic type and a dynamic value — that equals nil only when both halves are empty.

That is enough for a correct answer. What follows is what you add when the interviewer digs.

If the interviewer digs deeper

To "how is an interface built" answer "two words". "A type or a method table and a pointer to the value; sixteen bytes against eight for a pointer — that is how it is made today, the specification promises only the pair." Everything else follows from that, and the interviewer hears it.

Explain the nil trap with the rule, not the example. "An interface equals nil only when both words are empty; here the second is empty and the first holds the type." The example comes after — it takes two lines and confirms rather than replaces.

On boxing say "the second word is a pointer". "A pointer goes in as is, zero allocations; a struct has to be placed somewhere, one. Small integers are the exception, the runtime keeps a ready array of 0 to 255." That last sentence shows you ran it.

On the cost of a call give a number and a cause. "I got four times on an empty method, and what is dear is not the table lookup but the lost inlining." And immediately: how it was measured — two concrete types in a slice, otherwise the compiler devirtualises.

On x.(T) and type switch say "the same thing". "Both compare a type pointer; the form is chosen by the number of branches, not by speed." Saying "switch is faster" with no number sounds worse than "there is a gap, but it is a fifth on an empty operation".

On method sets name the reason, not the rule. "A value in an interface has no address, so a method with a pointer receiver is not in its method set."

Next they ask

Next they ask

Can interfaces be compared with ==?

Short answer

They can, but carefully. The specification: two interface values are equal if they have identical dynamic types and equal dynamic values, or if both are nil. That is, both halves of the pair are compared.

The danger is that the comparison can panic at run time. If an uncomparable type sits inside — a slice, a map, a function — the comparison gives comparing uncomparable type. The compiler will not catch it: it sees only the interface. So == on an any holding external data is a source of panics, and there reflect.DeepEqual, or a comparison after a type assertion, is safer.

Next they ask

Where should an interface be declared — beside the implementation or at the consumer?

Short answer

The Go idiom is at the consumer, and it follows directly from implementation being implicit: a type needs to know nothing about an interface to satisfy it. So an interface can be declared where it is needed, at exactly the size needed.

The practical consequence: a one- or two-method interface beside the function that accepts it is normal and right. A large interface beside its single implementation is almost always a spare layer — it abstracts nothing but forces you to change two places instead of one.

Next they ask

What is the empty interface for and how does it differ from any?

Short answer

It does not: any is an alias for interface{}, introduced in Go 1.18 for readability. They are literally the same thing, and modern gofmt rewrites one into the other itself.

Its meaning is simple: every type satisfies the empty interface, because it demands nothing. The first word is still filled, though — with a type descriptor rather than a method table. So any is 16 bytes too, and the typed-nil trap works on it exactly the same way.

Next they ask

What is an itab and when is it created?

Short answer

An itab is what sits in the first word of an interface with methods: a "type plus interface" pair with the addresses of the method implementations beside it. The call target is taken from there.

It is built once per "concrete type — interface" pair and cached by the runtime, so repeated assignments of the same type into the same interface build nothing anew. Hence the answer to a common follow-up: the cost of assigning into an interface is the cost of boxing the value, not of building the table.

Next they ask

How do you check at compile time that a type implements an interface?

Short answer

With a stub line:

GO
var _ Stringer = (*Point)(nil)

Nothing is allocated and nothing runs here — it is an assertion for the compiler. If *Point stops implementing Stringer, the build fails in the package where the type is declared rather than where it is used.

The trick is worth knowing not for elegance: without it the error turns up in someone else's package six months later, pointing at a line nobody touched.

Next they ask

Interface over a value or over a pointer — what should go in?

Short answer

Look at two questions. First, the method set: if even one method has a pointer receiver, a pointer has to go in or it will not compile. Second, size: boxing a struct is a heap allocation, boxing a ready pointer is zero.

Hence the usual choice: if the type already lives behind a pointer, put the pointer in — it is both cheaper and more uniform. Putting a value in makes sense for small immutable types, where the copy is a feature rather than a cost.

Common misconceptions

Claim

an interface is just a pointer to an object

Actually

Semantically it is a pair — a dynamic type and a dynamic value — and in the current Go implementation two machine words. Measurable: any and Stringer take 16 bytes while *Point takes 8. The first word is the type or the method table, the second a pointer to the value.

Claim

if a nil pointer sits inside an interface, the interface is nil too

Actually

No: an interface equals nil only when both words are empty. The language FAQ says it verbatim — an interface value is nil only if the V and T are both unset. The second word is empty but the first holds the type, and err == nil is false.

Claim

the err != nil check is reliable on its own

Actually

It is reliable only if a concrete error type is never put into error. A function returning error but keeping a *MyErr inside will return an "error" even when there is none. Cured by declaring the variable with the interface type and returning nil explicitly in the success branch.

Claim

putting a value into an interface always allocates

Actually

Not always: a ready pointer goes into the second word as is — zero allocations. Small integers are zero too: the runtime keeps a ready array of 0 to 255. An allocation appears where the boxed value does not fit in a word — a struct, for instance.

Claim

a call through an interface is dearer because the method is looked up in a table

Actually

The table lookup is pennies. What is dear is that the compiler stops seeing the target and does not inline the call: a direct call is optimised together with the surrounding code, while a call through a table stays a call. Measured on an empty method: 11,461 ns against 2,804, four times.

Claim

a type switch is noticeably faster than a type assertion

Actually

Both forms do the same thing — compare a pointer to a type descriptor. On equal work it came out at 3,661–4,128 against 4,521–5,023 ns: the gap is real and stable, but it is a fifth on an empty operation. The form is chosen by how many types must be told apart, not by speed.

Claim

if a type has the method, it implements the interface

Actually

Only if the method is in the method set of what you are assigning. A method with a pointer receiver is not in the method set of the value: a copy in an interface has no address, and such a method could not change the original. Hence Point does not implement Stringer for a struct and success for its address.

Claim

interfaces can be compared freely with ==

Actually

Comparing interfaces is legal but panics at run time if an uncomparable type sits inside — a slice, a map or a function: comparing uncomparable type. The compiler will not see it, it knows only the interface. For values from external data, comparing after a type assertion is safer.

Practice

Two problems. Answer first, then check against the real output: in both, the correct answer is taken from a run of the script rather than assigned.

Practice · predict the output

A function is declared as returning error but inside returns a variable of a concrete type. What does this code print?
type NotFound struct{ Key string }

func find(ok bool) error {
var missing *NotFound
if ok {
	return nil
}
return missing
}

err := find(false)
fmt.Println(err == nil)
var p *NotFound
fmt.Println(p == nil)
fmt.Println(reflect.TypeOf(err), err == nil)
fmt.Println(find(true) == nil)

Practice · estimate

An Area() method over 4,096 shapes. How many times dearer is a call through an interface than a direct call to the same method?
times

Knowledge check

Question 1 of 6

A function returns error and inside returns a variable of type *MyErr that equals nil. What is err == nil at the caller?

Sources & further reading

4 SOURCES

  1. The Go specification — Interface types, Type assertions, Comparison operatorsOfficial documentation. The rules every behaviour here is derived from. On comparison: «Two interface values are equal if they have identical dynamic types and equal dynamic values or if both have the value nil». On type assertions: «For an expression x of interface type, but not a type parameter, and a type T, the primary expression x.(T) asserts that x is not nil and that the value stored in x is of type T».https://go.dev/ref/spec#Interface_types
  2. Go FAQ — Why is my nil error value not equal to nil?Official documentation. The answer to the most common interview question, given by the language authors themselves: «Under the covers, interfaces are implemented as two elements, a type T and a value V», and then the decisive part: «An interface value is nil only if the V and T are both unset».https://go.dev/doc/faq#nil_error
  3. The Laws of Reflection — the Go blogOfficial documentation. The wording about the representation of an interface variable, from which this whole topic grows: «a variable of interface type stores a pair: the concrete value assigned to the variable, and that value's type descriptor».https://go.dev/blog/laws-of-reflection
  4. Effective Go — Interfaces and methodsOfficial documentation. On why interfaces are small in the first place: «Interfaces in Go provide a way to specify the behavior of an object: if something can do this, then it can be used here». Hence the idiom of declaring an interface on the consumer's side rather than beside the implementation.https://go.dev/doc/effective_go#interfaces_and_types