Skip to content
academia.sh

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 reflect level, 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.

Start typing to search.

↑↓ ↵ Esc navigate · open · close