Go Interview Prep #1: Interfaces

Interfaces are one of the most distinctive parts of Go’s type system and reliably show up in interviews. The core idea is small but the implications go deep.

What is an interface?

An interface defines a set of method signatures. Any type that implements all those methods satisfies the interface — no implements keyword needed. This is called structural typing or duck typing.

type Stringer interface {
    String() string
}

type User struct{ Name string }

func (u User) String() string { return u.Name }

// User satisfies Stringer automatically
var s Stringer = User{Name: "Alice"}

How interfaces work at runtime

An interface value is a two-word struct: (type, data). The type word points to an itab (interface table) with runtime metadata about the concrete type and method pointers; the data word holds the value (or a pointer to it). This is why interface comparisons, type assertions, and reflection all work.

Calling a method through an interface is an indirect call via the itab — slightly more expensive than a direct call, which matters in tight loops but is rarely a concern at API boundaries.

Method sets (value vs pointer receivers)

Whether a type implements an interface depends on its method set:

ReceiverMethod set of TMethod set of *T
func (t T) M()has Mhas M
func (t *T) M()no Mhas M

So if M has a pointer receiver, only *T (not T) satisfies an interface that requires M:

type Counter struct{ n int }

func (c *Counter) Inc() { c.n++ }

type Incrementer interface{ Inc() }

// var i Incrementer = Counter{}  // compile error
var i Incrementer = &Counter{}    // ok

Rule of thumb: if a method mutates the receiver, use a pointer receiver. Be consistent across the type’s methods so the method sets stay predictable.

The empty interface

any (alias for interface{} since Go 1.18) has no methods, so every type satisfies it. It is used when a function must accept any value. Prefer generics when you only need “some type” without losing type safety:

// Prefer this when possible
func Identity[T any](v T) T { return v }

// Keep any for true heterogeneous values (e.g. fmt, encoding/json)
func Print(v any) { fmt.Println(v) }

Type assertions and type switches

A type assertion extracts the concrete value from an interface:

var v any = "hello"

s, ok := v.(string) // safe: ok = true, s = "hello"
n, ok := v.(int)    // ok = false, n = 0 — no panic

Without the comma-ok form, a failed assertion panics: s := v.(string).

A type switch handles multiple types cleanly:

switch x := v.(type) {
case string:
    fmt.Println("string:", x)
case int:
    fmt.Println("int:", x)
default:
    fmt.Printf("unknown: %T\n", x)
}

Interface composition

Interfaces can embed other interfaces. io.ReadWriter is the canonical example:

type ReadWriter interface {
    io.Reader
    io.Writer
}

Keep interfaces small — one or two methods is ideal. The smaller the interface, the easier it is to satisfy and the more types can use it. The standard library lives by this: io.Reader, io.Writer, fmt.Stringer, error.

Common interview questions

Q: Can a nil pointer satisfy an interface?
Yes. A nil *T can be assigned to an interface that *T implements. The interface value is not nil (its type word is set), but calling a method on it invokes the method with a nil receiver. This is the nil interface trap.

Q: What is the difference between a nil interface and a typed nil?
A nil interface has both type and data set to zero. A typed nil has a concrete type in the type word but nil in the data word. Only the former compares equal to nil. Always return an untyped nil (or a properly non-nil error value) from functions that return error.

Q: When should you define an interface?
At the point of use, not at the point of implementation. Define the interface in the package that consumes the behavior, not the package that provides it. This avoids circular imports and keeps interfaces minimal. Prefer “accept interfaces, return structs.”

Q: Why does T not implement an interface that *T does?
Because methods with pointer receivers are only in the method set of *T. The compiler will not auto-take the address of a non-addressable value to satisfy an interface.

Q: Are interface comparisons reliable?
Two interface values are equal if their dynamic types are identical and their dynamic values compare equal. Comparing interfaces that hold incomparable dynamic types (slices, maps, functions) panics.

Quick reference

ConceptKey point
Implicit satisfactionNo implements, just match the method set
Runtime representation(itab, data) pair
Method setsPointer-receiver methods: only on *T
Type assertionv.(T) — use comma-ok to avoid panic
Empty interfaceany — prefer generics when type safety helps
Interface sizePrefer one-method interfaces
Define where?At the consumer (point of use)