Interface Fundamentals

Learn the core concepts of Go interfaces - how to declare them, understand interface values, and work with the empty interface.

Go approaches polymorphism differently from languages built around classes and inheritance. There is no implements keyword, no explicit hierarchy, and no ceremony involved in declaring that a type satisfies a contract. Instead, Go uses interfaces — types that specify a set of method signatures. Any concrete type that possesses all the methods listed in an interface automatically satisfies it, with no declaration in the type's source code.

This implicit satisfaction is the engine behind much of Go's flexibility. A function that accepts an interface parameter can receive any concrete value that meets the method set, which makes it straightforward to swap implementations, test with mocks, and compose behaviour from small pieces. Because the compiler enforces that the full interface is met at compile time, you get the safety of static checking without the rigidity of a formal type hierarchy.

The rest of this page covers the three foundations you need to use interfaces fluently: how to declare an interface type, what the empty interface (interface{}) means and when to reach for it, and what actually lives inside an interface value when your program runs.

Interface Type Declarations

An interface type looks like a list of method names followed by their parameter lists and return types. No method body appears inside the declaration — the interface only describes what a type must be able to do, never how it does it.

type Speaker interface {
    Speak() string
}

Speaker now represents any type in your program that has a method Speak with no parameters and a string return. There is no need for the type to mention Speaker anywhere. If the method exists with the right signature, the type implicitly satisfies the interface.

type Dog struct {
    Name string
}
func (d Dog) Speak() string {
    return d.Name + " says woof!"
}
type Cat struct {
    Name string
}
func (c Cat) Speak() string {
    return c.Name + " says meow!"
}

Both Dog and Cat now satisfy Speaker. A function that expects a Speaker can accept either one without knowing which concrete type it received.

func Announce(s Speaker) {
    fmt.Println(s.Speak())
}
func main() {
    d := Dog{Name: "Rex"}
    c := Cat{Name: "Misty"}
    Announce(d) // Rex says woof!
    Announce(c) // Misty says meow!
}

The Announce function works purely with the behaviour the interface promises. It never touches Name or any other field — it only calls Speak. This separation between "what data the type holds" and "what the type can do" is what makes interfaces useful for decoupling. The type's internal representation can change entirely without affecting code that depends only on the interface.

Implicit satisfaction confirmed at compile time:

If you pass a value to Announce and the type does not have a Speak() string method, the program will not compile. The compiler catches missing methods immediately, long before any runtime error could occur. This compile-time check is one reason Go's structural typing (checking that all methods exist) differs from runtime duck typing — the programme won't start if the contract isn't met.

Pointer receivers and method sets

A method defined with a pointer receiver belongs to the pointer type, not the value type. This matters for interface satisfaction.

type Mute struct{}
func (m *Mute) Speak() string {
    return "..."
}

*Mute satisfies Speaker, but Mute does not. If you write Announce(Mute{}), the compiler will reject it because Mute has no Speak method — only *Mute does. The fix is to pass a pointer: Announce(&Mute{}).

Pointer receivers can silently block interface satisfaction:

A common mistake is to define all methods on a type with value receivers, then change one to a pointer receiver for efficiency. Suddenly code that passed a value of the type to an interface stops compiling. When you see a compile error stating that a concrete type does not implement an interface, check whether the method set includes the methods on the value type or only on the pointer type.

Beginners often think in terms of "which types implement this interface." In Go, a more productive mental model is: a value of type T can be stored in an interface variable I if the method set of T contains all methods of I. The method set of T includes methods with value receivers; the method set of *T includes both value and pointer receivers. So if you always work with pointer receivers, you always need to pass a pointer to satisfy the interface.

The Empty Interface

An interface that declares zero methods is written interface{}. Because it demands nothing of a value, any type — custom structs, built-in types, even nil — satisfies it. The empty interface is Go's way of saying "this could be absolutely anything."

var anything interface{}
anything = 42
fmt.Println(anything) // 42
anything = "hello"
fmt.Println(anything) // hello
anything = struct{ X, Y int }{3, 4}
fmt.Println(anything) // {3 4}

From Go 1.18 onward, the predeclared identifier any is an alias for interface{}. The two are interchangeable, but any often reads more naturally in generic code and function signatures.

The most visible use of the empty interface is fmt.Println. Its signature accepts a variadic slice of interface{}, which is why you can pass strings, integers, and custom types to it all in the same call. Similarly, encoding/json uses interface{} to represent arbitrary JSON structures before you decode them into typed structs.

Working with the value inside

Storing a value in an empty interface discards all type information the compiler had. To recover it and use the concrete value, you need a type assertion or a type switch. The simplest safe pattern looks like this:

var val interface{} = "gopher"
s, ok := val.(string)
if ok {
    fmt.Println("string:", s)
} else {
    fmt.Println("not a string")
}

If the assertion fails, the program doesn't panic — ok is false and s gets the zero value of string. This is the only way to extract the concrete value safely without knowing the type beforehand.

Empty interface removes all compile-time safety:

Storing everything as interface{} turns the compiler's type checker off for that value. A common anti-pattern among developers new to Go is to use map[string]interface{} for all data, mimicking dynamic languages. This forces you to write type assertions everywhere, pushes errors to runtime, and makes the code harder to reason about. Use the empty interface only when the shape of the data genuinely cannot be known at compile time — for example, when decoding unstructured JSON or writing a generic container. In all other cases, prefer concrete types or named interfaces with specific method sets.

Interface Values

An interface variable is not a reference to a concrete value plus a note saying "this is a Dog." Under the hood, it stores two pieces of information: a pointer to the concrete value (or the value itself, if it fits) and a pointer to the type descriptor of that concrete value. Together, these two pointers form what is called an interface value.

Conceptually, you can think of an interface variable as a pair (type, value).

var s Speaker                // (nil, nil)
s = Dog{Name: "Rex"}        // (main.Dog, Dog{Name:"Rex"})
s = Cat{Name: "Misty"}      // (main.Cat, Cat{Name:"Misty"})

When you call s.Speak(), the runtime uses the type pointer to look up the correct method implementation and calls it with the concrete value. This is dynamic dispatch — the decision of which function to run happens at runtime, not compile time. The overhead is small but not zero; Go's choice to keep interfaces lightweight (they require no explicit declaration on the implementing type) means the dispatch is handled through a compact internal table.

The nil interface confusion

Because an interface value is a pair, it can be nil in two different ways. The whole pair is nil only when both the type and the value are nil — that is, when no concrete value has ever been assigned to the interface variable.

var s Speaker
fmt.Println(s == nil) // true

But if you assign a nil concrete pointer to the interface, the type pointer becomes non-nil while the value pointer stays nil. The interface variable as a whole is no longer nil.

var d *Dog = nil
var s Speaker = d
fmt.Println(s == nil) // false

s is not nil because the type information (*Dog) is present, even though the dog it points to is nil. This trips up error handling frequently. A function that returns a nil *MyError stored in an error interface will cause err != nil to be true even though the underlying pointer is nil.

type AppError struct{}
func (e *AppError) Error() string { return "app error" }
func doWork() error {
    var e *AppError = nil
    return e
}
func main() {
    err := doWork()
    if err != nil {
        fmt.Println("error occurred") // Printed, even though e is nil
    }
}

Never return a nil concrete error pointer as an error interface:

The idiomatic fix is to return nil directly as the error value, not a nil pointer of a concrete type that implements error. In the example above, doWork should return nil instead of return e. If you must return a concrete type, return a value type rather than a pointer, or explicitly check for nil and return nil when appropriate. This pattern is so common that every Go developer eventually encounters it, and a linter like staticcheck can catch it automatically.

To check whether an interface value holds a nil concrete pointer, you must use a type assertion that inspects the underlying value:

if d, ok := s.(*Dog); ok && d == nil {
    fmt.Println("holds a nil *Dog")
}

Empty interface values are represented differently:

Non-empty interfaces (those with methods) use an internal structure called iface, which includes the method table alongside the type and value pointers. The empty interface (interface{}/any) uses a simpler structure called eface that omits the method table because there are no methods to dispatch. Assigning and passing empty interface values is slightly cheaper than non-empty ones, though the difference is rarely noticeable outside hot loops. This internal distinction also matters when using the reflect package — empty interfaces are treated as a special case.

Beginners can build an accurate mental model by remembering that an interface variable is a two-part container: the type label and the actual data. Asking "is this container empty?" (== nil) checks whether both slots are unset. Assigning anything — even a nil pointer with a concrete type — fills the type slot, so the container is no longer empty.


The three ideas on this page — declaring interfaces, the empty interface, and interface values — are the substrate on which all other Go interface features are built. Implicit satisfaction keeps your types decoupled from the abstractions they fulfill; the empty interface provides an escape hatch for truly dynamic data; and understanding the internal (type, value) pair prevents the most painful nil-related surprises.