Interface Embedding
How to compose Go interfaces from smaller ones using embedding, with practical examples from the standard library and testing
What Interface Embedding Is
Go interfaces describe behavior — a set of method signatures a type must have. Interface embedding is a way to build a new interface by including one or more existing interfaces inside it. The new interface inherits all the methods from the embedded ones, plus any you add yourself. No special syntax beyond writing the interface name inside the declaration block.
You are not creating a runtime relationship between interfaces. The compiler flattens everything into a single list of required methods. The embedded names serve as a declaration of intent: “this new interface is a Reader, a Writer, and also a Closer.”
This mechanism is at the heart of how the standard library stays readable while defining dozens of composite I/O contracts — io.ReadWriter, io.ReadWriteCloser, io.ReadSeeker, and more. It keeps method signatures from being repeated all over the codebase.
Basic Syntax and a Worked Example
Write the name of the interface you want to embed inside the declaration block of the new one. The most famous example is io.ReadWriter.
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
}
Any type that has both Read and Write methods with the correct signatures automatically satisfies ReadWriter. The standard library’s os.File is the classic case: it implements Read and Write on its pointer receiver, so *os.File is an io.ReadWriter without a single extra line of code.
Implicit satisfaction still applies:
You don’t need to write “implements ReadWriter” anywhere. If the type has the methods, it matches. Embedding only changes the interface definition; it doesn’t alter how satisfaction works.
How the Compiler Handles Embedding
When you embed an interface, the compiler takes the full method set of the embedded interface and adds it to the new one. If you embed multiple interfaces, it merges their method sets. The result is a flat set of signatures — no nesting at runtime, no chain of lookups.
Consider a slightly larger composition:
type Scanner interface {
Scan() string
}
type Closer interface {
Close() error
}
type ScanCloser interface {
Scanner
Closer
}
The compiler treats ScanCloser exactly as if you had written:
type ScanCloser interface {
Scan() string
Close() error
}
The two forms are semantically identical. The embedded version is shorter, and it tells the reader that the interface is built from two independent concerns. A *TextScanner with Scan and Close methods on its pointer receiver will match ScanCloser automatically.
type TextScanner struct {
data []string
idx int
}
func (t *TextScanner) Scan() string {
if t.idx >= len(t.data) {
return ""
}
val := t.data[t.idx]
t.idx++
return val
}
func (t *TextScanner) Close() error {
t.data = nil
return nil
}
var sc ScanCloser = &TextScanner{data: []string{"a", "b"}}
Because the embedding is flattened at compile time, there is no runtime overhead. The interface value still contains a concrete type and a method table, and that table contains all the required methods directly — no delegation through embedded interface names.
Embedding is not inheritance:
Interface embedding does not create a type hierarchy. ScanCloser is a new, standalone interface type. It is not a subtype of Scanner or Closer — though a value of ScanCloser can be passed to a function expecting Scanner because the method set is a superset.
Duplicate Methods and the Go 1.14 Fix
When you embed multiple interfaces, it is entirely possible that two of them declare a method with the same name. The compiler allows this only if the signatures are identical. In that case, the merged method set contains a single copy of the method. If the signatures differ, the code fails to compile.
Before Go 1.14, even identical signatures caused a “duplicate method” compile error when they arrived through different embedding paths. For example, io.ReadCloser embeds Reader and Closer, and io.WriteCloser embeds Writer and Closer. If you tried to define io.ReadWriteCloser by embedding both:
type ReadWriteCloser interface {
io.ReadCloser
io.WriteCloser
}
The Close method would appear twice — once from each path — and the compiler would reject it. Since Go 1.14, identical signatures are merged correctly, so the above definition works and is now used in some internal code. The simpler definition without the conflict remains fine:
type ReadWriteCloser interface {
Reader
Writer
Closer
}
Name collisions are still a hazard with different signatures:
If two embedded interfaces have a method with the same name but different parameter lists or return types, the compiler will refuse to compile the merged interface. No concrete type could possibly satisfy both constraints simultaneously. This is the compiler protecting you from an impossible contract.
Standard Library Examples
The standard library relies on interface embedding to build clear, composable contracts.
io.Reader / io.Writer and their composites
The io package defines fundamental interfaces and then combines them without repeating method signatures:
io.ReadWriter— embedsReader,Writerio.ReadCloser— embedsReader,Closerio.WriteCloser— embedsWriter,Closerio.ReadWriteCloser— embedsReader,Writer,Closerio.ReadSeeker— embedsReader,Seeker
Each of these communicates its intent in one line. A function that needs reading and seeking writes io.ReadSeeker as the parameter type, and any type with Read and Seek works. The caller doesn't need to guess which combination of single-method interfaces is expected.
net.Error
The net package defines a network error interface that embeds the built-in error interface:
type Error interface {
error
Timeout() bool
Temporary() bool
}
The embedding tells you immediately that a net.Error is an error — you can use it anywhere an error is accepted, and you can call Error() to get the message string. The extra methods add network-specific information about whether the failure was a timeout or a temporary condition.
container/heap.Interface
The heap package requires implementers to satisfy sort.Interface plus two additional methods. Instead of writing out Len, Less, and Swap again, it embeds sort.Interface:
type Interface interface {
sort.Interface
Push(x interface{})
Pop() interface{}
}
A glance at this definition shows that a heap must first be sortable, and then adds the push/pop operations. The alternative — writing all five methods explicitly — would obscure that relationship.
Embedded interfaces are part of the public contract:
When you embed an interface, any change to the embedded interface’s method set automatically propagates to the embedding interface. If sort.Interface ever added a fourth method, heap.Interface would immediately require it as well. This ensures composite interfaces stay consistent with their building blocks.
Embedding Interfaces in Your Own Code
When you design a package, building interfaces from smaller parts gives you precise control over what your functions demand. A function should ask for the smallest set of capabilities it actually uses.
Suppose you are building a caching layer. You might define:
type Fetcher interface {
Fetch(key string) ([]byte, error)
}
type Putter interface {
Put(key string, value []byte) error
}
type ReadWriteCache interface {
Fetcher
Putter
}
A function that only reads from the cache takes a Fetcher. One that updates it takes a ReadWriteCache. A concrete implementation like *RedisStore can implement both, and it works everywhere. The interface definitions stay short, and callers see exactly what is required.
Don’t embed an interface only to narrow it later:
It is tempting to define a large interface by embedding many small ones and then pass that large interface everywhere. That defeats the purpose. Functions should accept the narrowest interface that provides the methods they call. Embedding helps you compose the narrow ones; it doesn't mean you should always use the composite.
Using Embedding for Test Mocking
There is a complementary technique — embedding an interface inside a struct — that is widely used for testing. This is not the same as interface-in-interface embedding, but it builds on the same principle and is worth knowing.
You can create a mock struct that embeds the interface you want to fake, and then override only the methods the test cares about. The embedded interface provides default implementations (though they will be nil and panic if called), so you implement just the necessary methods.
Consider a UserStore interface and a service that uses it:
type UserStore interface {
GetByID(id string) (*User, error)
Save(user *User) error
}
In a test for a function that only calls GetByID, you can write:
type mockStore struct {
UserStore // embedded interface
user *User
err error
}
func (m mockStore) GetByID(id string) (*User, error) {
return m.user, m.err
}
The mock struct satisfies UserStore because GetByID is provided and Save is inherited from the embedded interface — though calling it would panic, which is fine because the test never calls it. This pattern keeps mock code minimal and avoids the boilerplate of implementing every method when only one is used.
This technique appears frequently in table-driven tests across the Go ecosystem. It is a practical application of embedding interfaces — not for composition of contracts, but for partial implementation of a contract during testing.
Common Mistakes and Misconceptions
Embedding does not create runtime dispatch chains:
Some developers new to Go think that when ReadWriter embeds Reader, the runtime first checks for ReadWriter, then for Reader. That is not how it works. The compiled interface value has a flat method table. Embedding is a compile-time convenience, not a runtime hierarchy.
Forgetting that embedded methods are part of the contract:
If you embed Reader and Writer into MyInterface, any type that satisfies MyInterface must implement Read and Write. It is easy to read past the embedded names and miss a method when writing a concrete type. The compiler will catch it, but the error message might reference the method name, not the embedding path. Knowing the interface’s full method set from the flattened view is essential.
Over-embedding can hide the true shape of an interface:
A chain of embedded interfaces can make it hard to see the complete method set at a glance. While the compiler resolves everything, humans reading the code may need to trace through several files. In library code, this is a trade-off: the reduced duplication is worth the indirection. In application code, prefer flatter, simpler interfaces when possible.
Summary
Interface embedding gives Go a way to compose contracts without duplicating method signatures. It flattens at compile time into an ordinary interface with a flat method set, so there is no runtime cost. The standard library uses it pervasively to build precise I/O and sorting contracts from small, single-responsibility interfaces.
When you define your own interfaces, start with the smallest meaningful ones and embed them to express broader requirements. This keeps your code readable and your function signatures honest. If a type already satisfies the smaller interfaces, it will automatically satisfy the composite — no extra work needed.