Implicit Interface Satisfaction

An interface in Go is simply a set of method signatures. What makes Go different from Java, C#, or TypeScript’s class-based interfaces is that satisfying an interface is implicit: a type never writes “implements” or “extends” anything. If a type has methods whose names, parameters, and return types match an interface’s method set, that type satisfies the interface automatically — the compiler works this out purely by comparing signatures, with no explicit declaration anywhere. This single design decision is what makes Go’s interfaces so flexible, and understanding it well is essential to reading and writing idiomatic Go.

Overview: How Implicit Satisfaction Works

In a nominally-typed language you declare intent up front: class Circle implements Shape. The compiler then checks that promise once, at the class definition. Go uses structural typing instead: there is no promise to check, because there is no declaration to make. Anywhere a value is used where an interface type is expected — as a function argument, a return value, or in a variable assignment — the compiler looks at the method set of the value’s concrete type and asks “does this include every method the interface requires?” If the answer is yes, the value is accepted; if not, you get a compile error at that usage site. Nothing about the concrete type’s own definition needs to mention the interface at all.

This has a powerful practical consequence: interfaces can be defined after the types that will satisfy them, even in a completely different package, including packages you don’t control. The standard library leans on this constantly — io.Writer is a single-method interface, and dozens of unrelated types across the standard library and third-party code satisfy it without ever importing the io package. You can retrofit an interface onto existing code just by writing a new interface type near wherever you need to consume that behavior.

Under the hood, a value stored in an interface variable is represented as a pair: a pointer to a type descriptor (which identifies the concrete type and its method table) and the underlying data (a pointer to it, or the value itself for things that fit efficiently). An interface value is only truly nil when both parts are empty — no type and no data. That distinction is the root of one of Go’s most notorious gotchas, covered in Common Mistakes below.

Method sets also depend on whether a method has a value receiver or a pointer receiver. A method declared with a value receiver, like func (r Rectangle) Area() float64, belongs to the method set of both Rectangle and *Rectangle. A method declared with a pointer receiver, like func (p *Person) Greet() string, belongs only to the method set of *Person — a plain Person value does not satisfy an interface that requires Greet. This trips up newcomers constantly, so it gets its own worked example below.

Because the check happens at the usage site rather than at the type’s definition, error messages about a missing method often appear far away from the type itself — at the line where you tried to pass it to a function or assign it to an interface variable. That is normal and expected in Go; it is simply where the compiler first needed the guarantee to hold.

Syntax

An interface type is declared with the interface keyword and a list of method signatures. No type ever references the interface by name in order to satisfy it.

type InterfaceName interface {
	MethodOne(paramType) returnType
	MethodTwo(paramType, paramType) (returnType, error)
}
Part Meaning
interface Keyword that starts the type’s method set definition.
Method signatures Name, parameter types, and return types only — no bodies, and parameter names are optional.
(nothing else) There is no clause anywhere for a concrete type to declare it satisfies InterfaceName.

Any type — a struct, a named numeric type, a map type, a function type — satisfies InterfaceName the moment it has methods matching every signature listed.

Examples

Example 1: A basic Shape interface

package main

import "fmt"

type Shape interface {
	Area() float64
}

type Rectangle struct {
	Width  float64
	Height float64
}

func (r Rectangle) Area() float64 {
	return r.Width * r.Height
}

type Circle struct {
	Radius float64
}

func (c Circle) Area() float64 {
	return 3.14159 * c.Radius * c.Radius
}

func printArea(s Shape) {
	fmt.Printf("Area: %.2f\n", s.Area())
}

func main() {
	r := Rectangle{Width: 3, Height: 4}
	c := Circle{Radius: 2}
	printArea(r)
	printArea(c)
}

Output:

Area: 12.00
Area: 12.57

Rectangle and Circle never mention Shape anywhere in their definitions. Both simply define an Area() float64 method, which is enough for printArea to accept either one through the Shape parameter. The compiler checked this at the call sites printArea(r) and printArea(c).

Example 2: Pointer receivers and method sets

package main

import "fmt"

type Greeter interface {
	Greet() string
}

type Person struct {
	Name string
}

func (p *Person) Greet() string {
	return "Hello, " + p.Name
}

func main() {
	p := &Person{Name: "Ava"}
	var g Greeter = p
	fmt.Println(g.Greet())
}

Output:

Hello, Ava

Greet has a pointer receiver, so it belongs only to the method set of *Person, not Person. That is why g is assigned p (a *Person) rather than a plain Person value — assigning a value directly would fail to compile, as shown in Common Mistakes.

Example 3: A realistic multi-type interface

package main

import "fmt"

type Notifier interface {
	Notify(message string) string
}

type EmailNotifier struct {
	Address string
}

func (e EmailNotifier) Notify(message string) string {
	return fmt.Sprintf("Emailing %s: %s", e.Address, message)
}

type SMSNotifier struct {
	Phone string
}

func (s SMSNotifier) Notify(message string) string {
	return fmt.Sprintf("Texting %s: %s", s.Phone, message)
}

func broadcast(message string, notifiers []Notifier) {
	for _, n := range notifiers {
		fmt.Println(n.Notify(message))
	}
}

func main() {
	notifiers := []Notifier{
		EmailNotifier{Address: "ava@example.com"},
		SMSNotifier{Phone: "555-0100"},
	}
	broadcast("Server is down", notifiers)
}

Output:

Emailing ava@example.com: Server is down
Texting 555-0100: Server is down

broadcast takes a slice of Notifier, and two completely unrelated struct types — EmailNotifier and SMSNotifier — sit in that slice side by side. Neither type was written with the other, or with Notifier, in mind; they just happen to both have a matching Notify method. This is implicit satisfaction doing real work: the caller decides which types play the Notifier role, not the types themselves.

Catching mistakes early: a compile-time assertion

Because there’s no explicit “implements” declaration, it’s easy to accidentally drift a type’s method signature away from an interface it’s meant to satisfy, and only discover it far away at a call site. A common idiom guards against this: declare a blank-identifier variable that forces the compiler to check the relationship immediately, right next to the type.

package main

import "fmt"

type Shape interface {
	Area() float64
}

type Square struct {
	Side float64
}

func (s Square) Area() float64 {
	return s.Side * s.Side
}

var _ Shape = Square{}

func main() {
	sq := Square{Side: 5}
	fmt.Println(sq.Area())
}

Output:

25

The line var _ Shape = Square{} creates no real variable — it exists purely so the compiler checks, at that exact line, that Square satisfies Shape. If someone later renames Area to area by mistake, this line fails to compile immediately, right next to Square‘s definition, instead of somewhere far away.

How It Works Step by Step

When the compiler encounters a place where a concrete value is used as an interface — an assignment, a function call, a return statement — it performs the same check every time:

  • It determines the static (compile-time) type of the expression being used, for example Rectangle or *Person.
  • It computes that type’s method set: value receivers contribute to both T and *T; pointer receivers contribute only to *T.
  • It compares that method set against every method the target interface requires, matching name, parameters, and return types exactly.
  • If every required method is present, the assignment is allowed and the compiler builds an interface value holding a type descriptor plus the underlying data.
  • If any method is missing or its signature doesn’t match exactly, compilation fails right there with a “does not implement” error.

This check is entirely a compile-time affair. At runtime, calling a method through an interface value (s.Area() in Example 1) uses the type descriptor stored inside the interface value to look up and call the correct concrete method — this is dynamic dispatch, and it’s why the same line of code can call Rectangle.Area or Circle.Area depending on what’s actually stored inside s at that moment.

Common Mistakes

Mistake 1: Assigning a value where only the pointer type satisfies the interface

Because Greet in Example 2 has a pointer receiver, only *Person — not Person — satisfies Greeter. Assigning a plain value fails to compile:

type Greeter interface {
	Greet() string
}

type Person struct {
	Name string
}

func (p *Person) Greet() string {
	return "Hello, " + p.Name
}

func main() {
	var g Greeter = Person{Name: "Ava"} // compile error: Person does not implement Greeter
	fmt.Println(g.Greet())
}

The fix is to take the address of the value, giving the compiler a *Person whose method set actually includes Greet:

package main

import "fmt"

type Greeter interface {
	Greet() string
}

type Person struct {
	Name string
}

func (p *Person) Greet() string {
	return "Hello, " + p.Name
}

func main() {
	var g Greeter = &Person{Name: "Ava"}
	fmt.Println(g.Greet())
}

Output:

Hello, Ava

Mistake 2: The typed-nil interface trap

Because an interface value is only truly nil when both its type and data parts are empty, returning a nil pointer through an interface-typed variable produces a non-nil interface — a classic Go surprise:

package main

import "fmt"

type MyError struct{}

func (e *MyError) Error() string {
	return "something went wrong"
}

func doWork(fail bool) *MyError {
	if fail {
		return &MyError{}
	}
	return nil
}

func run(fail bool) error {
	var err *MyError = doWork(fail)
	return err
}

func main() {
	e := run(false)
	fmt.Println(e == nil)
}

Output:

false

Even though doWork(false) returns a nil *MyError, wrapping it in the error interface at the return err line stamps a type descriptor (*MyError) onto the interface value, so it is no longer the “totally empty” nil. The fix is to check the concrete pointer before it ever gets wrapped in the interface, and return a literal nil instead:

package main

import "fmt"

type MyError struct{}

func (e *MyError) Error() string {
	return "something went wrong"
}

func doWork(fail bool) *MyError {
	if fail {
		return &MyError{}
	}
	return nil
}

func run(fail bool) error {
	err := doWork(fail)
	if err == nil {
		return nil
	}
	return err
}

func main() {
	e := run(false)
	fmt.Println(e == nil)
}

Output:

true

The general rule: never return a concrete pointer type directly as an interface-typed value without first checking it for nil — do the check while the type is still concrete.

Best Practices

  • Keep interfaces small — often a single method. The standard library’s io.Writer, io.Reader, and fmt.Stringer are all one-method interfaces, and small interfaces are trivially satisfied by more types.
  • Define interfaces on the consumer side (near the function that needs them), not the producer side. You don’t need an interface at all until something needs to accept multiple implementations.
  • Use var _ InterfaceName = ConcreteType{} (or &ConcreteType{} for pointer receivers) right after a type’s methods to catch signature drift at compile time, next to the type instead of far away.
  • Once a type has any pointer-receiver method, make all of its methods pointer receivers for consistency, and pass pointers to it everywhere, including into interfaces.
  • Never return a nil concrete pointer directly as an interface value; check it for nil first while its type is still concrete, then return a literal nil.
  • Favor accepting interfaces as function parameters and returning concrete types — this keeps callers flexible while keeping the function’s own promises precise.
  • Don’t design interfaces speculatively “in case you need another implementation someday.” Add the interface when a second implementation actually exists or is imminent.
  • Document an interface’s expected behavior (not just its signatures) in a comment above it, since Go has no other place to record the contract you intend implementers to honor.

Practice Exercises

  • Define an interface Describer with a single method Describe() string. Implement it for two different struct types of your choice (for example Book and Movie), then write a function PrintAll(items []Describer) that prints each item’s description on its own line.
  • Take Example 2’s Person type and change Greet to use a value receiver instead of a pointer receiver. Write a short program that assigns both a Person{} value and a &Person{} pointer to a Greeter variable, and confirm both compile now that the receiver is a value receiver.
  • Write your own version of the typed-nil example with a different error type and a different function name. Before running it, predict on paper whether comparing the returned error to nil will be true or false, then verify by tracing through the code by hand.

Summary

  • Go interfaces are satisfied implicitly: a type never declares which interfaces it implements — the compiler just compares method sets at each usage site.
  • This is structural typing, and it lets interfaces be defined after, and separately from, the types that satisfy them, even across package boundaries.
  • An interface value internally holds a type descriptor plus the underlying data; it is nil only when both are empty.
  • Value receivers add a method to both T and *T‘s method sets; pointer receivers add it only to *T‘s.
  • Wrapping a nil concrete pointer in an interface produces a non-nil interface value — always check nil-ness on the concrete type before returning it as an interface.
  • Use var _ Interface = Type{} to enforce satisfaction at compile time, right where a type is defined.