Why Go Interfaces Are Implicitly Satisfied (No implements Keyword)

Go interfaces are implicitly satisfied when a type defines all required methods, eliminating the need for an explicit implements keyword.

Why Go Interfaces Are Implicitly Satisfied

You write a function that accepts a Shape. You pass a Circle. The compiler accepts it. You didn't write implements. You didn't register the type in a global map. You didn't add a decorator. You just added the Area() method to Circle.

This feels like magic if you come from Java or C#. It feels familiar if you come from Python or JavaScript, where duck typing rules everything. Go gives you the flexibility of duck typing with the safety of compile-time checking. The compiler verifies that a type has the required methods before the program runs. If the methods exist, the interface is satisfied. No declaration needed.

This design is called structural typing. Go cares about the shape of your type, not its name or its lineage. If the shape matches the interface, the type fits.

Structural typing and the form analogy

Think of an interface as a form with required fields. The form asks for a Name and an Email. You submit a document. The clerk checks the document. If the document has a Name field and an Email field, the clerk accepts it. You don't need to stamp "THIS IS A NAME-EMAIL FORM" on the document. The presence of the fields is enough.

Go interfaces work the same way. The interface lists the methods. The compiler checks if the type has those methods. If the methods are there, the type satisfies the interface. The compiler doesn't care what the type is called. It doesn't care if the type is a struct, a pointer, or a named type. It only cares about the method set.

This approach keeps code decoupled. You can add interface satisfaction to a type without modifying the type's definition. You can satisfy an interface defined in a third-party package without touching that package. The contract is defined by the methods, not by a registration step.

Minimal example

Here's the smallest proof: define an interface, write a struct with the matching method, and assign it.

package main

import "fmt"

// Speaker defines the behavior required to speak.
type Speaker interface {
	Speak() string
}

// Dog has a Speak method.
type Dog struct{}

// Speak returns the sound a dog makes.
func (d Dog) Speak() string {
	return "Woof"
}

func main() {
	// Dog satisfies Speaker implicitly because it has Speak() string.
	var s Speaker = Dog{}
	fmt.Println(s.Speak())
}

The compiler checks Dog against Speaker. Dog has Speak() string. The interface requires Speak() string. The match is exact. The assignment compiles. At runtime, s holds an interface value that wraps the Dog instance. Calling s.Speak() dispatches to Dog.Speak.

How the compiler and runtime handle interfaces

The compiler performs two checks. First, it verifies the method set. It looks at all methods defined on the type and compares them to the interface. The signatures must match exactly. The parameter types, return types, and method names must align. If a method is missing or a signature differs, the compiler rejects the program.

Second, the compiler generates code to box the value into an interface. An interface value in Go is a pair of pointers. One pointer points to the type information. The other pointer points to the data. When you assign Dog{} to Speaker, the compiler creates this pair. The type pointer points to the internal representation of Dog. The data pointer points to the Dog value on the stack or heap.

At runtime, method calls use the type pointer. The runtime looks up the method in the type's method table and calls it. This is dynamic dispatch. The cost is small. The interface value carries the type info, so the runtime knows exactly which function to call.

This mechanism allows heterogeneous collections. A slice of Speaker can hold Dog, Cat, and Robot instances. Each element is an interface value with its own type and data pointers. Iterating over the slice and calling Speak() dispatches to the correct method for each element.

Realistic example: a notifier system

Here's a realistic pattern: a function that processes a list of notifiers, accepting any type that can send a message.

package main

import "fmt"

// Notifier defines the contract for sending messages.
type Notifier interface {
	Send(msg string) error
}

// EmailNotifier implements Send for email.
type EmailNotifier struct {
	to string
}

// Send prints the email action.
func (e EmailNotifier) Send(msg string) error {
	fmt.Printf("Email to %s: %s\n", e.to, msg)
	return nil
}

// SlackNotifier implements Send for Slack.
type SlackNotifier struct {
	channel string
}

// Send prints the Slack action.
func (s SlackNotifier) Send(msg string) error {
	fmt.Printf("Slack #%s: %s\n", s.channel, msg)
	return nil
}

The function and usage demonstrate how implicit satisfaction enables flexible composition.

// NotifyAll iterates over a slice of Notifiers.
func NotifyAll(notifiers []Notifier, msg string) {
	for _, n := range notifiers {
		// Dispatch to the concrete Send method.
		_ = n.Send(msg)
	}
}

func main() {
	// Mix concrete types in a slice of the interface.
	ns := []Notifier{
		EmailNotifier{to: "admin@example.com"},
		SlackNotifier{channel: "alerts"},
	}
	NotifyAll(ns, "Server is down")
}

NotifyAll depends only on Notifier. It doesn't know about EmailNotifier or SlackNotifier. You can add SMSNotifier or WebhookNotifier without changing NotifyAll. As long as the new type has Send(msg string) error, it fits. This is the power of implicit satisfaction. The function is open for extension but closed for modification.

Pitfalls and method set rules

Implicit satisfaction introduces subtle rules about method sets. The method set of a type determines which interfaces it satisfies. A type T has a method set consisting of all methods with receiver T. The method set of *T includes all methods with receiver T plus all methods with receiver *T.

This distinction matters. A value can only satisfy an interface if all required methods have value receivers. A pointer can satisfy an interface even if some methods have pointer receivers.

If you define Speak on *Dog, only *Dog satisfies Speaker. Dog does not. The compiler rejects the assignment with cannot use Dog{} (type Dog) as type Speaker in assignment: Dog does not implement Speaker (method Speak has pointer receiver).

The fix is to pass a pointer. var s Speaker = &Dog{} works because *Dog has the method. Or change the receiver to a value receiver if the method doesn't need to modify the struct.

Another pitfall is shadowing. If you define a method with the wrong signature, the interface is not satisfied. Speak() int does not satisfy Speak() string. The compiler catches this, but the error can be confusing if you're scanning a large file. The error message points to the assignment site and lists the missing or mismatched methods.

Convention aside: receiver names should be short and match the type. Use (d Dog) or (e EmailNotifier). Avoid (this Dog) or (self Dog). The community expects one or two letters. It keeps the code readable and consistent.

Convention aside: use var _ Interface = Concrete{} to enforce the contract at compile time. This idiom creates a variable that is never used, so the compiler checks the assignment but the optimizer removes the variable. It's a common pattern in library code to ensure types satisfy interfaces without runtime overhead.

Convention aside: accept interfaces, return structs. Functions should accept interface parameters to allow flexibility. They should return concrete types to expose the full API. This mantra keeps the boundary clear. Callers pass interfaces. Libraries return structs.

Decision matrix

Use an interface when you need to accept multiple types that share behavior. Use a concrete type when the function only works with one specific implementation. Use a small interface when you want to compose behavior flexibly. Use a pointer receiver when the method modifies the struct or the struct is large. Use var _ Interface = Concrete{} when you want to enforce the contract at compile time without runtime overhead. Use a value receiver when the method reads the struct and the struct is small.

Interfaces are contracts, not hierarchies. The compiler checks the shape. Trust the check.

Where to go next