AlgoMaster Logo

Implicit Interface Satisfaction

Last Updated: May 22, 2026

High Priority
10 min read

In Go, a type satisfies an interface the moment its method set lines up with the interface's required methods. There is no implements keyword, no explicit declaration, no annotation linking the two. The compiler checks the methods, and if they match, the relationship exists. This lesson covers the mechanics of structural satisfaction, the practical consequences for code organization, and the patterns Go developers use to catch satisfaction mistakes early.

The Core Idea: Structure, Not Declaration

Consider two payment types, CreditCardPayment and PayPalPayment, that satisfy a Payable interface. Look closely at their definitions and you can see something missing: neither struct ever names the Payable interface. The struct definitions don't reference it. The methods don't reference it. The interface itself doesn't list its implementers. The relationship exists only because the methods happen to line up.

The CreditCard definition stands alone. Nothing in it knows about Payable. The link is established by the compiler when it sees the assignment var method Payable = &CreditCard{...}: it checks whether *CreditCard has every method Payable requires, finds that it does, and accepts the assignment. If you delete the Payable interface entirely, CreditCard still works as a type; it just no longer satisfies any interface, because there's no interface for it to satisfy.

This is called structural typing. The type's structure (specifically, its method set) determines what it can be used as. Many languages take the opposite approach: a class declares which interfaces it implements, and the compiler checks that those interfaces are honored. Java uses implements Runnable, C# uses : IDisposable. Python takes a third route, "duck typing", where there's no static check at all and a mismatched method fails at runtime.

Go sits between those two. The check is fully static, but the declaration is invisible. Structural and statically checked.

Why This Design Choice Matters

Saying "no implements keyword" sounds like a minor syntactic detail. The consequences run deep, and they shape how Go programs are organized.

When a type has to declare its interfaces upfront, the type's author has to know in advance every interface the type will ever satisfy. If a new interface gets added later in a different package, the original type can't be retrofitted without modifying its source. With implicit satisfaction, the type doesn't care. Any new interface defined anywhere will be satisfied automatically as long as the methods line up.

This flips the usual ownership question. In a language with explicit interfaces, the type owns the relationship: it lists the interfaces it implements. In Go, the interface owns the relationship: any type with the right methods is in. The interface can be defined long after the type exists, in a completely different package, and the relationship still works.

EmailNotifier was written without any awareness of Notifier. The interface came later, perhaps in another package, perhaps written by another team. Because *EmailNotifier happens to have a Send(string) error method, the assignment into []Notifier works. No edit to EmailNotifier was required, and the two pieces of code don't need to know about each other.

Interfaces Defined Where They're Used

Implicit satisfaction encourages a specific design pattern: define an interface in the package that consumes it, not in the package that produces values. The consuming package describes the minimum behavior it needs, and any producing type with that behavior plugs in automatically.

Look at the standard library for examples. The io package defines io.Reader. It doesn't define implementations. Implementations live in os (for files), bytes (for buffers), strings (for strings), net/http (for response bodies), and dozens of other packages. None of those packages imports io.Reader to declare conformance. They just have a Read([]byte) (int, error) method, and io.Reader accepts them all.

countBytes works against io.Reader. The actual reader is a *strings.Reader created by strings.NewReader. The strings package doesn't depend on io.Reader for conformance reasons; it just exposes a type that happens to have a Read method. The match is invisible to the type's author.

The pattern this enables: keep your interfaces small, define them next to the code that uses them, and let producers be unaware. A test for countBytes can pass a fake reader that returns a canned response, no production code needs to know.

Method Sets Decide Who Satisfies What

Method sets determine which type, the value type or the pointer type, has a particular method available, and that decision flows directly into interface satisfaction. A quick recap so the rules make sense in this context:

ReceiverMethod on T?Method on *T?
Value receiver func (x T)YesYes
Pointer receiver func (x *T)NoYes

Now layer interfaces on top. If an interface requires a method, only the types whose method sets include that method satisfy the interface. Pointer-receiver methods are in the method set of the pointer type but not the value type, so the value type may fall short of satisfying the interface even though the pointer type does.

Notify has a pointer receiver. The method set of EmailSender (the value type) does not include Notify, so the commented-out line doesn't compile. The error is EmailSender does not implement Notifier (Notify method has pointer receiver). The method set of *EmailSender does include Notify, so assigning the pointer works.

The flip side: if all the interface's methods have value receivers, both the value type and the pointer type satisfy the interface, because the pointer type's method set is a superset of the value type's.

Both Book and *Book satisfy Priceable here, because the value receiver puts Price in the method set of both. This is one reason value receivers are the default choice for read-only methods: you keep options open at the call site.

When the interface mixes value and pointer receiver methods, the rule still follows the method set. Any method with a pointer receiver forces callers to use the pointer type to satisfy the interface, because the value type's method set won't include it.

Choosing pointer receivers when you don't need them narrows what can satisfy your interface. Choosing value receivers when the method must mutate state is a correctness bug. Pick based on what the method does, not on what feels nicer at the call site.

The Compile-Time Satisfaction Check Pattern

Implicit satisfaction is a feature, but it has one drawback: nothing tells you that a type is supposed to satisfy an interface. If you change the interface, or accidentally change a method signature on the type, the satisfaction breaks. The compiler error only appears at the call site where you tried to use the type as the interface, which might be in a different file or package.

Go developers use a one-line idiom to make the assertion explicit and force the check at compile time:

That line declares a variable of type Payable, assigns it the value (*CreditCard)(nil) (a nil pointer of type *CreditCard), and discards the variable by assigning to the blank identifier _. The line generates no runtime cost; the compiled binary doesn't include it. Its entire purpose is to make the compiler verify, right there in the source file, that *CreditCard satisfies Payable.

If you change CreditCard.Pay to take a different parameter type, or rename it, or delete it, the line fails to compile with a clear error pointing at exactly this spot. Without the line, the failure would surface somewhere else, possibly in a test that doesn't run on every build.

Place the assertion near the type's method definitions, so anyone modifying the methods sees the contract right there. Some codebases put it in the test file next to the type's tests; both placements work. The standard library uses this pattern heavily. Search for var _ io.Writer = in the Go source tree and there are dozens of examples.

For value-type satisfaction, use the value form:

This forces the compiler to check that Book (the value type) satisfies Priceable. If only *Book satisfies it, the line fails, which is exactly the signal you want before the failure surfaces somewhere subtle.

The compile-time assertion takes zero bytes in the compiled binary because nothing references the discarded variable. Use it liberally for any type that's expected to satisfy an interface.

Retrofitting Interfaces Around Existing Types

Implicit satisfaction unlocks a pattern that's hard or impossible in languages with explicit interfaces: defining a new interface around types you don't own or can't modify.

When you're a third-party library that provides a Receipt type with a Total() float64 method. Months later, you write your own OrderSummary type, also with a Total() float64 method. You'd like a single function that can produce a summary line for either one. In a language with explicit interfaces, you'd be stuck: you can't go back and add implements Totaller to a type defined in someone else's library.

In Go, you define the interface on your side and both types are automatically in:

Totaller is defined in your code, used in your function, and satisfied by types from two different sources, neither of which knows the interface exists. The Receipt type wasn't designed for Totaller; it just happens to have the right method. That's the only thing that matters.

This pattern is everywhere in real Go programs. A logger interface defined locally that the standard library's log.Logger and your custom logger both satisfy. A Cacher interface that wraps both redis.Client (from a third-party package) and an in-memory map. A test seam that lets a function take either a real database handle or a fake one written in twenty lines. None of those types need to coordinate; the interface emerges from the shape they already have.

How One Type Lights Up Many Interfaces

Because satisfaction is structural, a single type can satisfy several interfaces at once just by having the right combination of methods. The type doesn't have to declare them. Add a String() string method and the type satisfies fmt.Stringer. Add a Close() error method and it also satisfies io.Closer. Add a Read([]byte) (int, error) method and it now also satisfies io.Reader. Each interface looks at the methods it needs and ignores everything else.

*DigitalWallet has three methods and satisfies three interfaces. None of the interfaces need to know about each other, and none of them need to know about DigitalWallet. The grouped var (... ) block keeps the assertions tidy when a type satisfies several interfaces.

The diagram below sketches the relationship. A type's method set forms a "shape" in capability space, and every interface whose required methods are a subset of that shape is automatically satisfied.

The single type on the left exposes three methods. Each interface in the middle requires one of those methods and accepts the type automatically. Code on the right that uses each interface gets to work with the same concrete type without coordinating with the other consumers. Add a fourth method to DigitalWallet and any new interface that needs that method picks it up too, without any edit to the existing code.

This is the practical payoff of implicit satisfaction. Capabilities accumulate naturally on a type, and interfaces slice them up into whatever views the consuming code needs. The type stays simple, the interfaces stay small, and adding a new view never requires touching the type.

When Implicit Satisfaction Surprises You

The flexibility of implicit satisfaction comes with one real downside: accidental satisfaction. If two unrelated interfaces happen to declare a method with the same name and signature, a type that meant to satisfy one will satisfy both. In practice this rarely causes problems, because the method name usually telegraphs the intent. A Pay(amount float64) error method isn't going to show up on a type that has nothing to do with payments. Still, the possibility exists, and it's about.

The other surprise is the inverse: a type that should satisfy an interface but doesn't, because a method signature is off by one parameter, or has the wrong return type, or uses the wrong receiver type. The compiler will catch this the moment you try to use the type as the interface, but the error message points at the call site, not at the type definition. The compile-time assertion pattern (var _ I = (*T)(nil)) puts the check next to the type, so the error appears in the right file when a method drifts.

This program compiles because nothing in main tries to use *WireTransfer as a Payable. The method exists, but the return type is string instead of error, so the type doesn't satisfy Payable. The mismatch is invisible until someone writes var x Payable = &WireTransfer{...}, and then the error appears at the assignment, possibly far from the type definition. Uncommenting the assertion pulls the check up to the type itself.

The compiler error for the failed assertion is clear:

The diagnostic shows both the actual signature and the expected one, which makes the fix obvious. This kind of feedback is exactly what the assertion exists to surface.