Interface Embedding in Go: Design and Patterns
Series: Go Embedding Features — Interface Embedding
1. What is Interface Embedding?
Interface embedding in Go allows one interface to embed one or more other interfaces, thereby extending or reusing interface definitions.
The embedded interface’s method sets are combined into the new interface, meaning the new interface implicitly contains all methods from the embedded ones.
Example:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// WriterCloser embeds Reader and Writer
type WriterCloser interface {
Reader
Writer
Close() error
}
The WriterCloser
interface includes Read
, Write
, and Close
methods. This makes interface definitions modular and easy to compose.
2. Use Cases for Interface Embedding
- Interface Reuse: Avoid re-declaring methods by reusing existing interface definitions.
- Interface Extension: Add new capabilities on top of existing interfaces.
- Multi-interface Composition: Combine responsibilities into more powerful interfaces.
- Flexible Design: Promotes modular, layered design of system components.
3. Syntax Details
- You can embed one or more interfaces and declare additional methods.
- The resulting method set includes all embedded interfaces plus any new ones.
- A type must implement all methods from the embedded interfaces to satisfy the new interface.
- Order of embedding does not affect behavior.
- The empty interface
interface{}
is often used, but embedding is best suited for structured capability composition.
4. Real-World Example
package main
import "fmt"
// Base interfaces
type Speaker interface {
Speak() string
}
type Mover interface {
Move() string
}
// Composite interface
type Animal interface {
Speaker
Mover
Sleep() string
}
// Implementing Animal
type Dog struct{}
func (d Dog) Speak() string {
return "Woof!"
}
func (d Dog) Move() string {
return "Run"
}
func (d Dog) Sleep() string {
return "Zzz"
}
func main() {
var a Animal = Dog{}
fmt.Println(a.Speak()) // Woof!
fmt.Println(a.Move()) // Run
fmt.Println(a.Sleep()) // Zzz
}
This example shows how to combine interfaces to define richer abstractions without duplication.
5. Underlying Mechanism and iface Layout
5.1 Method Set Composition Diagram
+---------------------+
| ReadWriter |
+---------------------+
| + Read(p []byte) | ← from Reader
| + Write(p []byte) | ← from Writer
+---------------------+
↑ ↑
| |
+-------+ +--------+
| Reader| | Writer |
+-------+ +--------+
| Read | | Write |
+-------+ +--------+
Explanation: The ReadWriter
interface includes methods from both Reader
and Writer
.
5.2 Internal Structure of iface
In Go, an interface variable (non-empty) is implemented internally as:
+-------------------------+
| iface |
+-------------------------+
| + tab (method table) | -------------------+
| + data (value pointer) | |
+-------------------------+ |
v
+----------------------------+
| itab (table) |
+----------------------------+
| + type descriptor |
| + interface descriptor |
| + func[0]: Read method |
| + func[1]: Write method |
+----------------------------+
tab
points to the method table (itab
).data
points to the underlying value.
When a method is called, it uses the function pointer from the itab
.
5.3 Method Call Flow
For a method call like:
rw.Write(p)
Internally, it’s compiled to:
iface.tab.func[1](iface.data, p)
- Function is resolved via
itab
. - First argument is the actual value (data).
- Remaining arguments are user inputs.
5.4 Multi-Level Interface Embedding
+-----------------------------+
| ReadWriteCloser |
+-----------------------------+
| + Read(p []byte) |
| + Write(p []byte) |
| + Close() |
+-----------------------------+
↑ ↑ ↑
| | |
+-------+ +--------+ +--------+
| Reader| | Writer | | Closer |
+-------+ +--------+ +--------+
| Read | | Write | | Close |
+-------+ +--------+ +--------+
ReadWriteCloser
collects all methods from Reader
, Writer
, and Closer
.
5.5 Effect on iface and eface
iface
is used for non-empty interfaces (those with methods). It has a method table.eface
is used for empty interfacesinterface{}
. It stores only type and data pointers.- Embedding enlarges the method set, so
itab
expands but the calling mechanism remains constant.
6. Patterns and Best Practices
6.1 Interface Segregation Principle
Split large interfaces into small, focused ones. Use embedding to compose broader ones.
6.2 Decorator Pattern via Embedding
Interfaces can be embedded to extend behavior without changing existing types.
6.3 Domain-Driven Design with Composable Interfaces
Define fine-grained domain behaviors and aggregate them into richer interfaces via embedding.
7. How Interface Embedding Works in the Compiler
- In
cmd/compile/internal/types/type.go
, theInterface
type represents all methods of an interface. - Embedded interfaces are recursively merged using
addInterfaceMethods
and related helpers. - During type checking (
typecheck
), the compiler ensures all embedded methods are implemented. - At runtime, Go builds complete method tables that reflect the final method set.
8. Conclusion
Interface embedding in Go is a powerful feature that enables interface reuse, modularity, and extension. Understanding how it works — both at the syntax and runtime level — is key to designing clean, extensible systems in Go.
By leveraging small, composable interfaces and embedding them thoughtfully, you can build maintainable and flexible software architectures.