Lesson 04 / 13
Interface Embedding
Embedding combines two interfaces into a single composite contract in the source, and this combination adds no new method at all: the same contract, built once by embedding and once rewritten by hand, wants the same two methods. The minimum method count a type must write to satisfy the composite interface equals the total method count of the embedded interfaces, no more, no fewer. Limiting measurement: embedding only combines the contract, not the implementation — two separate types can each satisfy their own separate contract, yet neither satisfies the composite contract.
Published Updated
In this lesson
- Measures with reflect that interface embedding combines two contracts at the declaration level in the source and adds no new method
- Asks the compiler to find the minimum method count a type must write to satisfy a composite interface
- Shows that a type satisfying a composite interface also satisfies each of the embedded interfaces
- Runs proof that combining a contract does not combine implementation — two separate types can each satisfy a separate contract while neither satisfies the composite one
Contents
The previous lesson saw a type able to satisfy more than one contract at the same time, but each contract stood separately in the source. When a type needs to both be able to store and be closable, a way is needed to refer to these two requirements with a single name. Embedding provides that way: an interface can take in other interfaces into its own body just by writing their names. This is unlike any of the bindings in the previous three lessons — explicitly written in the source, and so the clearest example of the declaration class.
Of the sixteen bindings in the first lesson, only four were in a declaration, and two of those
were already about embedding: Embedded itself and the compile-time proof that verified it.
This lesson grows those two lines and measures embedding’s own rules — what it combines, how
much it combines, and where it stops. Three measurements proceed in this order: first it is
confirmed that embedding really adds no new method; then how many methods are needed at
minimum to satisfy the composite contract is found; finally, where this combination stops —
in the contract or in the implementation — is tested.
Combining Contracts in the Source
An interface embedding is nothing more than writing another interface’s name inside an
interface body. The result is a new interface wanting the full set of methods from the
embedded interfaces. The syntax is short: no field name is placed in front of the embedded
interface, only its own name is written as a single line — the same writing rule embedding
borrows from struct embedding, a line that looks like an unnamed field but is actually a type
name. The following measurement builds the same contract two ways — once by embedding, once by
rewriting it by hand — and counts the method count both want with reflect.
// equivalence.go — a contract built with embedding asks for the same method count as one written by hand
package main
import (
"fmt"
"reflect"
)
type Reader interface{ Read() string }
type Writer interface{ Write(m string) }
// Embedding: two interface names are written into the embedding interface's body.
type ReadWriterEmbedded interface {
Reader
Writer
}
// The same contract, rewritten by hand.
type ReadWriterManual interface {
Read() string
Write(m string)
}
func main() {
a := reflect.TypeOf((*ReadWriterEmbedded)(nil)).Elem()
b := reflect.TypeOf((*ReadWriterManual)(nil)).Elem()
fmt.Println("method count wanted by embedding :", a.NumMethod())
fmt.Println("method count wanted by hand-writing:", b.NumMethod())
for i := 0; i < a.NumMethod(); i++ {
fmt.Println(" method:", a.Method(i).Name)
}
}
method count wanted by embedding : 2 method count wanted by hand-writing: 2 method: Read method: Write
ReadWriterEmbedded‘s source carries only two names (Reader, Writer), and writes no
method signature at all. But when reflect is asked, it reports wanting exactly two methods,
Read and Write, just like ReadWriterManual, which writes both methods by hand. Embedding
is a shortcut — it lets you write less in the source — but the contract it produces is
identical, byte for byte, to the one written by hand. This is an extension of the embedding
concept from the Go Fundamentals course’s Structs lesson: there a struct carried another
struct’s field and method names without writing them; here an interface carries another
interface’s method names without writing them. Both are called by the same word
(embedding) because both use the same mechanism: writing one name brings everything that
name carries along with it.
IN10. The syntax reflect.TypeOf((*T)(nil)).Elem() is the standard way to obtain an
interface type itself (not an interface value) as a reflect.Type — (*T)(nil) produces a
pointer value, .Elem() descends to the interface type that pointer points to. No real T
value is ever constructed throughout this measurement; only the type itself is queried.
Embedding is not limited to two interfaces here; an embedded interface can itself embed
another interface, and the chain can stretch several levels. The following measurement goes one
level deeper by embedding ReadWriter with a third interface (Closer) and tests that the
total method count is still summed correctly.
// chain.go — two-level embedding, the total method count still adds up correctly
package main
import (
"fmt"
"reflect"
)
type Reader interface{ Read() string }
type Writer interface{ Write(m string) }
type Closer interface{ Close() error }
type ReadWriter interface {
Reader
Writer
}
type FullSource interface {
ReadWriter
Closer
}
func main() {
t := reflect.TypeOf((*FullSource)(nil)).Elem()
fmt.Println("two-level embedding, total method count:", t.NumMethod())
for i := 0; i < t.NumMethod(); i++ {
fmt.Println(" method:", t.Method(i).Name)
}
}
two-level embedding, total method count: 3 method: Close method: Read method: Write
FullSource embeds ReadWriter, and ReadWriter itself embeds Reader and Writer — two
levels. The total method count is again three, the sum of the three individually embedded
methods (Read, Write, Close). Adding more levels does not change the summing rule: each
level carries every name below it upward, no name is ever lost, and no name is ever counted
twice.
This clarifies what someone reading FullSource needs to see: the source carries only two
lines (ReadWriter, Closer), yet the contract wants three methods. Reaching this count from
the source is not possible without opening the intermediate level (ReadWriter) — the reader
has to follow the whole chain. Embedding shortens the source, but it does not reduce the
reading burden by the same amount; a short source can still carry a long contract.
How Many Methods Have to Be Written
Since embedding adds no method, the minimum method count a type must write to satisfy a
composite interface should be the sum of the embedded interfaces’ methods — no more, no fewer.
reflect counted this by asking an interface type; the following measurement asks the same
question by testing a type against an interface definition, taken straight from the
compiler itself. Both should arrive at the same number: the count reflect reports is the
smallest count the compiler accepts.
// compiler.go — tests from inside the program whether a piece of source compiles
package main
import (
"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
}
// how-many-methods.go — the minimum method count needed to satisfy a composite interface is measured
package main
import "fmt"
func main() {
common := `package trial
type Reader interface{ Read() string }
type Writer interface{ Write(m string) }
type ReadWriter interface {
Reader
Writer
}
`
zeroMethods := common + `type Candidate struct{}
var _ ReadWriter = Candidate{}
`
oneMethod := common + `type Candidate struct{}
func (Candidate) Read() string { return "" }
var _ ReadWriter = Candidate{}
`
twoMethods := common + `type Candidate struct{}
func (Candidate) Read() string { return "" }
func (Candidate) Write(m string) {}
var _ ReadWriter = Candidate{}
`
fmt.Println("does it compile with zero methods:", compiles(zeroMethods))
fmt.Println("does it compile with one method :", compiles(oneMethod))
fmt.Println("does it compile with two methods :", compiles(twoMethods))
}
does it compile with zero methods: false does it compile with one method : false does it compile with two methods : true
The zero-method candidate does not compile, because both are missing. The one-method candidate
(only Read) does not compile either, because Write is still missing — satisfying Reader
does not mean satisfying ReadWriter; embedding does not let one interface stand in for the
other, it wants both together. The two-method candidate compiles, and this is exactly the
number reflect counted in the previous section. Three candidates test a single threshold (two
methods) from both directions: one short is not enough, the exact count is enough.
This threshold follows the same axis as the small-interface measurement in the third lesson:
the more methods an interface wants, the heavier the load on the type that satisfies it.
Embedding does not reduce the method count here; it only makes it easier to refer to two
separate contracts with a single name. The cost of satisfying ReadWriter is the sum of the
costs of satisfying Reader and Writer separately — embedding offers no shortcut that lowers
this sum, it only gathers the naming into one place.
IN11. All three candidates carry the same name (Candidate) and are tested against the
same ReadWriter; the only thing that changes is Candidate‘s own method set. This makes the
three measurements comparable — the difference sits in a single variable, everything else stays
fixed. The same discipline could also be tested with a fourth candidate — one writing three
methods, two matching the names ReadWriter wants and a third an extra method. A type like
this would still compile, because interface satisfaction is a subset relationship: as long
as a type’s method set covers what the interface wants, having more is not a problem.
Limiting Measurement: The Contract Combines, the Implementation Does Not
Embedding combines two contracts in the source, but this combination gives no implementation guarantee. A type satisfying the composite interface also satisfies the two embedded interfaces separately — that direction is easy. But the reverse is not true: two separate types can each satisfy two separate contracts separately, and neither satisfies the composite contract. The threshold measurement in the previous section asked “how many methods are enough” over a single type; this measurement asks the same question over more than one type, and shows there is no sharing at all between types.
// two-directions.go — the two directions of satisfying a composite contract are tested separately
package main
import "fmt"
type Reader interface{ Read() string }
type Writer interface{ Write(m string) }
type ReadWriter interface {
Reader
Writer
}
type File struct{}
func (File) Read() string { return "content" }
func (File) Write(m string) {}
type ReadOnly struct{}
func (ReadOnly) Read() string { return "a" }
type WriteOnly struct{}
func (WriteOnly) Write(m string) {}
func main() {
var f any = File{}
_, reads := f.(Reader)
_, writes := f.(Writer)
_, composite := f.(ReadWriter)
fmt.Println("forward direction, is File a Reader :", reads)
fmt.Println("forward direction, is File a Writer :", writes)
fmt.Println("forward direction, is File a ReadWriter :", composite)
var reader any = ReadOnly{}
var writer any = WriteOnly{}
_, readerOk := reader.(Reader)
_, writerOk := writer.(Writer)
_, readerCompositeOk := reader.(ReadWriter)
_, writerCompositeOk := writer.(ReadWriter)
fmt.Println("reverse direction, is ReadOnly a Reader :", readerOk)
fmt.Println("reverse direction, is WriteOnly a Writer :", writerOk)
fmt.Println("reverse direction, is ReadOnly a ReadWriter :", readerCompositeOk)
fmt.Println("reverse direction, is WriteOnly a ReadWriter :", writerCompositeOk)
}
forward direction, is File a Reader : true forward direction, is File a Writer : true forward direction, is File a ReadWriter : true reverse direction, is ReadOnly a Reader : true reverse direction, is WriteOnly a Writer : true reverse direction, is ReadOnly a ReadWriter : false reverse direction, is WriteOnly a ReadWriter : false
In the forward direction, a single type (File) satisfies all three interfaces — a type
satisfying the composite contract automatically also satisfies its parts, because the
composite contract’s method set is a superset of the parts. File having written its Read
and Write methods is enough on its own; it does not need to write different code three times
to separately satisfy Reader, Writer, and ReadWriter.
The reverse direction is different: ReadOnly satisfies only Reader, WriteOnly only
Writer, and both correctly. But neither satisfies ReadWriter — not ReadOnly, not
WriteOnly. The two contracts being satisfied separately does not mean the composite contract
is also satisfied; that requires a single type to write both methods together. Even if
both ReadOnly and WriteOnly exist in the program, a place wanting ReadWriter accepts
neither of them — there is no such thing as their “sum” in the type system.
IN12. ReadOnly and WriteOnly together, “somewhere,” satisfying both Read and Write
carries no meaning anywhere in the program — Go’s type system offers no mechanism that
automatically merges two separate types’ methods. Embedding only combines the interface
definition; a third type wanting to combine two separate concrete types’ behavior has to
either write its own methods or take both concrete types into itself through struct embedding,
as in the K01 Structs lesson.
This distinction explains the basic difference between struct embedding and interface embedding. When a struct embeds another struct, the embedded type’s real value — with its own fields and methods — sits concretely inside the outer struct; that was the reason the K01 Structs lesson’s embedded methods could be called without being written on the outer type. When an interface embeds another interface, there is no value at all involved, only a list of signatures is merged. Interface embedding carries no value, it only carries a request; who satisfies that request is a separate question, and the answer to that question can only be given by a single concrete type’s own methods (or that type’s own struct embeddings). Both kinds of embedding share the same word because both rest on the same idea — writing a name and gaining everything it carries — but one carries a value while the other carries only a contract.
Summary
- Interface embedding is nothing more than writing other interfaces’ names into an interface’s body; the binding stands explicitly in the source, the clearest example of the declaration class in this course.
- A contract built with embedding wants, at the
reflectlevel, exactly the same method count as a contract rewriting the same methods by hand — embedding adds no new method. - The minimum method count a type must write to satisfy a composite interface equals the sum of the embedded interfaces’: one short is not enough, the exact count is enough.
- A type satisfying the composite interface also satisfies each of the embedded interfaces, but not the reverse: two separate types can satisfy two separate contracts separately while neither satisfies the composite contract.
- Interface embedding carries no value, only a contract; struct embedding (the K01 Structs lesson) carries a real value — the same word, two separate mechanisms.
Next Step
In this lesson, 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 third lesson’s
Storer, and the first lesson’s Short, never appeared in the source of the type that
satisfied them — the party defining the contract was the party consuming it. The difference
between these two patterns is not an accident, it is a choice: sometimes the producing party
writes the contract (this lesson), sometimes the consuming party does (the first and third
lessons). The next lesson turns this choice into a design decision: who should define an
interface, the producing package or the consuming side, and does this have a measurable
counterpart?
To keep your progress and take notes, Log in
My notes
Log in to take notes.