Interface Embedding & Composition
Structs embed structs to reuse data and behavior. Interfaces embed interfaces to build bigger contracts out of smaller ones. You list an interface's name inside another interface, and its method set is folded in:
type Reader interface {
Read() string
}
type Writer interface {
Write(s string)
}
// ReadWriter is Reader AND Writer, composed.
type ReadWriter interface {
Reader
Writer
}
ReadWriter now requires both Read() and Write(s string). Writing it
this way is exactly equivalent to spelling out both methods by hand — but it
says what it's made of, and if Reader ever grows a method, ReadWriter
grows with it automatically.
The canonical example
This isn't a toy pattern; it's how the standard library is built. The io
package defines tiny interfaces and composes them:
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
type ReadWriter interface {
Reader
Writer
}
io.ReadWriter is literally io.Reader plus io.Writer. There's also
ReadCloser, WriteCloser, ReadWriteCloser — mix-and-match compositions of
the same handful of one-method interfaces.
Satisfying a composed interface
A type satisfies ReadWriter by implementing every method it gathers — nothing
special is required, and nothing is declared. If your type has both Read and
Write with the right signatures, it is a ReadWriter:
type Buffer struct{ data string }
func (b *Buffer) Read() string { return b.data }
func (b *Buffer) Write(s string) { b.data += s }
var rw ReadWriter = &Buffer{} // works: *Buffer has both methods
Because Write mutates the buffer, its receiver is a pointer, which means
*Buffer (not Buffer) carries the full method set — so you store &Buffer{}.
Composition over inheritance, again
This is the same design idea as struct embedding, aimed at contracts instead of
data. Go's culture is to keep interfaces small — often a single method — and
compose them only where a specific job needs the combination. A function that
only reads takes a Reader; one that reads and writes takes a ReadWriter. You
never force a caller to satisfy methods it doesn't use.
Contrast that with fat, inherited base interfaces where one giant type dictates everything a subtype must provide. Go's small-interfaces-composed approach means your types accidentally satisfy interfaces they were never written against, and callers ask for the narrowest contract that does the job. "The bigger the interface, the weaker the abstraction," goes the Go proverb — so you build big ones only by composing small ones, on demand.
Try it
You'll compose Reader and Writer into a ReadWriter, make a *Buffer
satisfy it, and write an echo function that accepts the composed interface —
proving a value can flow through the small pieces and the whole alike.
Practice
1. Compose a ReadWriter
IntermediateThe starter defines two one-method interfaces:
type Reader interface { Read() string } type Writer interface { Write(s string) }Three things to do:
-
Make
ReadWriterembed both interfaces (the starter embeds onlyReader, so it compiles but the tests fail):type ReadWriter interface { Reader Writer } -
Give the concrete
Bufferthe two methods so that*BuffersatisfiesReadWriter.Writemutates the buffer, so use a pointer receiver:func (b *Buffer) Read() string // returns the current contents func (b *Buffer) Write(s string) // appends s to the contents -
Implement
echo, which writessand then returns the buffer's contents:func echo(rw ReadWriter, s string) string
Behavior, called repeatedly on the same
*Buffer:echo(b, "hi")returns"hi"- then
echo(b, "there")returns"hithere"(writes accumulate)
The tests use
&Buffer{}through aReadWritervariable.package main import "fmt" // Reader and Writer are the two small interfaces we compose. type Reader interface { Read() string } type Writer interface { Write(s string) } // ReadWriter should embed BOTH Reader and Writer. The starter embeds only // Reader on purpose: the package still compiles (a *Buffer has a Read method, // so it satisfies a Reader-only interface), but the tests fail because echo // can't reach Write through this contract and its stub returns "". // TODO: embed Writer too, so ReadWriter is { Reader; Writer }. type ReadWriter interface { Reader } // Buffer is a concrete type backed by a string. Write mutates it, so its // methods take a pointer receiver and *Buffer is what satisfies ReadWriter. type Buffer struct { data string } func (b *Buffer) Read() string { return b.data } func (b *Buffer) Write(s string) { b.data += s } // echo writes s into rw, then returns the current contents via Read. func echo(rw ReadWriter, s string) string { // TODO: implement — write s, then return rw.Read() return "" } func main() { b := &Buffer{} fmt.Println(echo(b, "hi")) }Sign in to write and run your solution in the browser.
Sign in for hints and the solution.
-