Go Embedding Series — 02. Interface Embedding in Go Design and Patterns

| Categories Go  | Tags Go  Interface Embedding  Design Patterns  Composition  Interface-Oriented Programming  Go Embedding Series 

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 interfaces interface{}. 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, the Interface 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.