Lesson 05 / 13
Interface Design
Consumer-side definition is measured: a four-method type is written without referencing any interface at all, four separate consumers unaware of each other each define their own single-method interface, and the same type satisfies all four — the producer's source has zero references to any of these four names. Limiting measurement: when the same contract is defined under two separate names in two consumers, they appear unable to stand in for each other, yet the type system accepts both directly against the same value.
Published Updated
In this lesson
- Measures that a type satisfies four separate consumer interfaces that never appear in its source
- Shows directly from code that the producer's source has zero references to the consumer interfaces
- Runs proof that the same method signature, defined under two separate names in two separate interfaces, accepts the same value in both
- Measures the cost of consumer-side definition through two unaware names implicitly standing in for each other
Contents
The previous lesson opened with an example where the party defining the contract acted
before the type that would satisfy it: Reader and Writer were already there before
File was written. But the first lesson’s Short and the third lesson’s Storer did the
opposite — neither ever appeared in the source of the type that satisfied it. This lesson
turns that observation into a design decision: who should define an interface, the producing
side or the consuming side, and does this choice have a measurable counterpart?
This question is not a preference, it is the natural extension of the first lesson’s second claim. If the source never says a type satisfies an interface, that interface does not need to exist in the producing side’s source at all — the consumer can write a contract the producer never knew about and apply it to the same type afterward. This lesson shows that possibility once more, this time at scale and with a cost attached: how many separate contracts can a single type be wrapped in by how many independent consumers, and what invisibility appears in exchange for that flexibility.
Consumer-Side Definition
The following measurement writes a four-method type (Store) without referencing any
interface at all. Then four separate consumers, unaware of each other, each define their own
single-method interface. The measurement counts how many of these four interfaces Store
satisfies.
// store.go — the producer side's code is written without referencing any interface at all
package main
import "fmt"
type Store struct{ items []string }
func (s *Store) Add(o string) { s.items = append(s.items, o) }
func (s *Store) Remove(o string) bool { return true }
func (s *Store) List() []string { return s.items }
func (s *Store) Size() int { return len(s.items) }
// Four separate consumers' own interfaces, each unaware of the others.
type Writer interface{ Add(o string) }
type Cleaner interface{ Remove(o string) bool }
type Reporter interface{ List() []string }
type Measurable interface{ Size() int }
type Backer interface{ Backup() error }
func main() {
var s any = &Store{}
_, isWriter := s.(Writer)
_, isCleaner := s.(Cleaner)
_, isReporter := s.(Reporter)
_, isMeasurable := s.(Measurable)
_, isBacker := s.(Backer)
count := 0
for _, ok := range []bool{isWriter, isCleaner, isReporter, isMeasurable, isBacker} {
if ok {
count++
}
}
fmt.Println("is Writer :", isWriter)
fmt.Println("is Cleaner :", isCleaner)
fmt.Println("is Reporter :", isReporter)
fmt.Println("is Measurable :", isMeasurable)
fmt.Println("is Backer :", isBacker)
fmt.Println("count satisfied out of five candidates:", count)
}
is Writer : true is Cleaner : true is Reporter : true is Measurable : true is Backer : false count satisfied out of five candidates: 4
Store’s source is four lines: Add, Remove, List, Size. None of the names Writer,
Cleaner, Reporter, Measurable appear in these four lines — these four interfaces might
not even have existed yet when Store was written. Despite this, Store satisfies all four,
because the only condition for satisfaction is the method signatures matching. The fifth
candidate (Backer) was added deliberately: it wants a method (Backup) Store never wrote,
and returns false — the measurement shows not only what fits but also what does not, so the
result is a real boundary and not a cherry-picked sample.
Each of the four interfaces represents a different consumer scenario: Writer is the need of a
piece of code that only adds new items, Cleaner of a function that only removes, Reporter
of a view that only lists, Measurable of a check that only tests size. These four consumers
may never have seen each other, may have been written in separate packages by separate teams —
the only thing they have in common is Store itself. Yet each can still build its own narrow
contract and fit Store to it, because conformance checking is done from each consumer’s own
point of view rather than a central registry.
IN13. The four consumer interfaces each want a single method; this is a deliberate choice.
The first lesson’s small-interface measurement showed that as method count grows, the number of
satisfying types falls. Here the reverse is used: reducing method count to one nearly
guarantees Store satisfies every consumer with a single method — this is one reason
consumer-side definition works this often in practice.
In this arrangement the producing package is written once and never changes; each new consumer
defines its own interface for its own need. In the course’s terminology, this is the same
mechanism that moves the binding of interface satisfaction to the use site — exactly what
the first lesson’s Short and the third lesson’s Storer did. The difference is that this
lesson pulls the same idea out of a single example and turns it into a design observation: four
consumers, four different needs, four different names, and the producing side knows about none
of them. The Design Principles course’s interface-segregation principle presents this pattern
as advice; this lesson does not give advice, it counts the pattern.
The count itself also says something: Store satisfies four interfaces, yet zero, not
four, interface names appear in Store’s own source. With Store’s source never changing, a
new interface wanting any subset of Store‘s existing four methods can be defined at any time,
and Store keeps satisfying it too — the same rule holds for a fifth, sixth, tenth consumer,
and the producing side’s source never needs to know about it. Whoever writes Store does not
need to know how many consumers wrap it in how many different contracts.
The Consumer’s Own Composite
The fourth lesson measured that embedding combines contracts on the producing side:
Reader and Writer were already combined inside ReadWriter before File was written. The
same combination is possible on the consumer side too — only this time the combining party is
not the producer, it is the consumer itself.
// consumer-composite.go — the consumer gathers the producer type's four methods into its own contract
package main
import "fmt"
type Store struct{ items []string }
func (s *Store) Add(o string) { s.items = append(s.items, o) }
func (s *Store) Remove(o string) bool { return true }
func (s *Store) List() []string { return s.items }
func (s *Store) Size() int { return len(s.items) }
type Writer interface{ Add(o string) }
// A fifth consumer gathers the four methods into its own composite contract; it does not use
// embedding, it writes all four signatures by hand.
type FullStore interface {
Add(o string)
Remove(o string) bool
List() []string
Size() int
}
func main() {
var s any = &Store{}
_, isWriter := s.(Writer)
_, isFullStore := s.(FullStore)
fmt.Println("narrow contract, is Writer :", isWriter)
fmt.Println("wide contract, is FullStore :", isFullStore)
}
narrow contract, is Writer : true wide contract, is FullStore : true
FullStore gathers all four of Store‘s methods into a single contract, but not by embedding
— it writes all four by hand. FullStore is not itself an embedding chain. Despite this,
Store satisfies it too, exactly as it satisfies the single-method Writer. The fourth
lesson’s measurement showed how embedding combines a contract on the producing side; this
measurement shows the same combination can also be built on the consumer side, with the
producer never knowing about it. The two models do not clash here, they stand side by side on
the same type: Store satisfies both the narrowest contract (Writer) and the widest one
(FullStore) at the same time, because both want different subsets of the same four
methods.
IN14. FullStore wanting exactly the four methods Store really writes is a deliberate
choice — had the contract gone beyond the producer’s method set, Store would not have
satisfied it. What the measurement shows is not unlimited conformance: no matter how wide a
contract a consumer writes, it cannot go beyond what the producer actually wrote.
This clarifies the real boundary of consumer-side definition: the consumer cannot choose
what the producer writes, it can only choose which of the producer’s writings it wants.
Had FullStore wanted a fifth method (Backup), it would not have been satisfied, just like
Backer above — writing a wide contract does not substitute for adding a new method to the
producing side’s source. The freedom consumer-side definition gives is over the selection
of existing methods, not over the invention of methods that do not exist.
Limiting Measurement: The Same Contract, Two Separate Names
Consumer-side definition does not come free. When two separate consumers define two separate interfaces wanting the same method, these two interfaces appear to share no common point in the source at all — both stand in their own package, under their own name, unaware of each other. But Go’s type system compares interfaces by method set, not by name; this is why two separate names, as long as they carry the same signature, stand in for each other.
// two-names.go — two separate interface names carrying the same signature stand in for each other
package main
import "fmt"
type Store struct{ items []string }
func (s *Store) Add(o string) { s.items = append(s.items, o) }
// Two consumers, unaware of each other, define two interfaces with the same signature.
type Writer interface{ Add(o string) }
type Input interface{ Add(o string) }
func firstConsumer(w Writer) string { w.Add("A"); return "added through Writer" }
func secondConsumer(g Input) string { g.Add("B"); return "added through Input" }
func main() {
s := &Store{}
var g Input = s
var w Writer = g // can a value of type Input be assigned directly to a Writer variable
fmt.Println("was the Input value assignable to a Writer variable:", w != nil)
fmt.Println(firstConsumer(g)) // an Input value passes to a Writer parameter
fmt.Println(secondConsumer(w)) // a Writer value passes to an Input parameter
fmt.Println("total items added:", len(s.items))
}
was the Input value assignable to a Writer variable: true added through Writer added through Input total items added: 2
Writer and Input are two separate interfaces, written unaware of each other, carrying two
separate names. There is no embedding, no shared definition between them at all. Despite this,
the line var w Writer = g compiles — an Input value can be assigned directly to a Writer
variable — and firstConsumer accepts an Input value while expecting a Writer. The
difference is only in the name; Go’s type system defines interfaces not by their names but by
the methods they want. When two separate consumers define two separate interfaces wanting the
same method, these two definitions unintentionally become equivalent.
IN15. This line cannot be predicted just by reading the source: someone looking at the
Writer and Input definitions one at a time cannot see that a relationship exists between
them, because that relationship is written nowhere — it only surfaces when the two method sets
are compared. The measurement shows this blind spot by running it directly; reading the source
is not enough, the two definitions have to be placed side by side and their signatures
compared.
The boundary of this equivalence is also clear: the signatures have to be identical, mere resemblance is not enough. The following measurement asks this directly of the compiler.
// boundary.go — the same signature makes it valid, a different return type rejects it
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
)
func compiles(source string) bool {
dir, err := os.MkdirTemp("", "trial")
if err != nil {
return false
}
defer os.RemoveAll(dir)
os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module trial\n\ngo 1.24\n"), 0o644)
os.WriteFile(filepath.Join(dir, "code.go"), []byte(source), 0o644)
cmd := exec.Command("go", "build", "./...")
cmd.Dir = dir
cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
return cmd.Run() == nil
}
func main() {
common := `package trial
type Writer interface{ Add(o string) }
func f(w Writer) {}
`
sameSignature := common + `type Input interface{ Add(o string) }
func g(x Input) { f(x) }
`
differentSignature := common + `type Transmitter interface{ Add(o string) error }
func g(x Transmitter) { f(x) }
`
fmt.Println("same-signature interface valid :", compiles(sameSignature))
fmt.Println("different-signature interface valid :", compiles(differentSignature))
}
same-signature interface valid : true different-signature interface valid : false
Input carries exactly the same signature (Add(string)) as Writer, so it is accepted
where Writer is expected. Transmitter, on the other hand, carries the same name (Add)
but wants a different return type (error), and this difference alone is enough to break the
equivalence — the “does it compile” question turns to false. Equivalence looks not at the
name but at every part of the signature: method name, parameter types, return types, all
of them have to match together. Two interfaces independently written by two consumers only
become implicitly equivalent when both repeat the exact same contract word for word; even a
small difference breaks this implicit binding.
This is the cost of consumer-side definition: when independent consumers write independent contracts, nothing anywhere prevents the same contract from being repeated more than once, unaware of itself. The result is not wrong — the two interfaces really are equivalent, and the type system handles this correctly — but it is an invisible equivalence to someone reading the source. The same freedom that is useful when each of the four interfaces builds the narrowest contract for its own consumer turns into an extra contract when two consumers independently define the same need separately.
Summary
- In consumer-side definition, the producing side produces a type without writing any interface at all, and the consuming side defines its own interface according to its own need.
Storesatisfies all four consumer interfaces, and its source has zero references to any of the four names; a fifth candidate (Backer) goes unsatisfied and shows the boundary.- Small, single-method consumer interfaces make this pattern easier in practice — the lower the method count, the higher the odds of being satisfied.
- Go’s interface comparison looks at the method set, not the name; this is why two separately named interfaces carrying the same signature stand in for each other even with no connection written in the source.
- This is the cost of consumer-side definition: independent consumers can unknowingly repeat the same contract, and this repetition only becomes visible once the signatures are compared.
- The boundary of equivalence is exact: even if the method name is the same, if the parameter or return type differs, the two interfaces do not stand in for each other; the compiler rejects this difference directly.
Next Step
The consumer defining the interface is the natural consequence of a fact measured throughout this course: if the binding is never written in the source at all, who writes the contract is also not bound to the producing side’s decision. The frame measured across five lessons (declaration, use site, value) held here too: embedding sits in the declaration, consumer-side definition sits at the use site, and both are two different faces of the same implicit satisfaction principle. The next topic looks at where this model is used the most. In Go, an error is an interface, and every error value satisfies it implicitly — not a three-method or a four-method contract, but the narrowest possible contract with a single method. The next lesson’s question: where does an error value’s identity sit, and what is it that makes an error an error?
To keep your progress and take notes, Log in
My notes
Log in to take notes.