Interface embedding

Interface embedding in Go enables structs to inherit fields and methods from other types for efficient code composition.

Interface embedding

You are writing a function that processes data from a stream. The stream needs to support reading bytes, and it also needs to support closing the underlying resource when the work is done. You look at the standard library and find io.ReadCloser. You check its definition and see it embeds io.Reader instead of listing the Read method again.

Interface embedding is how Go composes contracts. It lets you build larger interfaces by combining smaller ones without repeating method signatures. The result is a single interface that requires a type to satisfy all the embedded methods plus any new ones you declare.

Embedding is not inheritance. There is no base class, no vtable manipulation, and no runtime overhead. The compiler treats an embedded interface as a declaration that the method set of the outer interface includes the method set of the inner interface. It is a compile-time expansion of requirements.

How embedding works

An interface in Go is defined by its method set. When you embed an interface inside another, the outer interface's method set becomes the union of all methods from the embedded interfaces and any methods declared directly.

Think of it like a job description. A "Senior Engineer" role might embed the "Engineer" role description. If a candidate satisfies the "Engineer" requirements and the extra "Senior" requirements, they satisfy the "Senior Engineer" role. The "Senior Engineer" description doesn't rewrite the "Engineer" tasks; it just references them.

// Reader defines the contract for reading bytes.
type Reader interface {
    // Read fills the buffer and returns the count or an error.
    Read(p []byte) (n int, err error)
}

// Closer defines the contract for releasing resources.
type Closer interface {
    // Close releases resources associated with the value.
    Close() error
}

// ReadCloser requires everything Reader and Closer require.
type ReadCloser interface {
    // Embedding Reader pulls in the Read method automatically.
    Reader
    // Embedding Closer pulls in the Close method automatically.
    Closer
}

The ReadCloser interface has two methods: Read and Close. The compiler expands the embedding during type checking. When you pass a value to a function expecting ReadCloser, the compiler checks if that value's type has both Read and Close with matching signatures. The embedding syntax is purely for the interface definition; it does not affect how the value is stored or passed.

Interface embedding is composition, not inheritance.

Minimal example

Here is a concrete type that satisfies the ReadCloser interface. The struct does not know about embedding. It just implements the methods.

package main

import "fmt"

// Buffer holds a slice of bytes and tracks the read position.
type Buffer struct {
    data []byte
    pos  int
}

// Read implements the Reader interface by copying data from the buffer.
func (b *Buffer) Read(p []byte) (n int, err error) {
    // Return EOF if we have consumed all data.
    if b.pos >= len(b.data) {
        return 0, fmt.Errorf("no more data")
    }
    // Copy available data into the destination buffer.
    n = copy(p, b.data[b.pos:])
    // Advance the position by the number of bytes copied.
    b.pos += n
    return n, nil
}

// Close implements the Closer interface by resetting the buffer.
func (b *Buffer) Close() error {
    // Reset position to simulate closing and reusing the buffer.
    b.pos = 0
    return nil
}

func main() {
    buf := &Buffer{data: []byte("hello")}

    // buf satisfies ReadCloser because it has Read and Close.
    var rc ReadCloser = buf

    // Use the embedded method through the interface.
    data := make([]byte, 5)
    n, _ := rc.Read(data)
    fmt.Printf("Read %d bytes: %s\n", n, string(data[:n]))

    // Call the Close method.
    rc.Close()
}

The variable rc holds an interface value. The dynamic type is *Buffer. The static type is ReadCloser. The compiler verified that *Buffer has Read and Close before allowing the assignment. The embedding in ReadCloser is invisible at runtime. The interface value stores a pointer to the type descriptor and a pointer to the data. The type descriptor lists the methods required by ReadCloser, which includes Read and Close.

The compiler checks the method set, not the name.

Realistic example

The io package is the master of interface embedding. It builds a hierarchy of reusable contracts that the entire standard library and third-party code rely on.

io.ReadWriter embeds both Reader and Writer. io.ReadWriteCloser embeds ReadWriter and Closer. This allows you to accept a value that can do everything without forcing the caller to implement a massive interface from scratch.

// ProcessStream reads data, processes it, and ensures the stream is closed.
func ProcessStream(rc io.ReadCloser) error {
    // Defer close to guarantee resource cleanup on exit.
    defer rc.Close()

    // Read data in chunks until EOF.
    buf := make([]byte, 1024)
    for {
        n, err := rc.Read(buf)
        // Handle partial reads and EOF correctly.
        if n > 0 {
            // Process the chunk here.
            _ = buf[:n]
        }
        if err != nil {
            // Return io.EOF as nil to signal successful completion.
            if err == io.EOF {
                return nil
            }
            return err
        }
    }
}

A *os.File implements io.ReadCloser because it has Read and Close methods. A *bytes.Buffer does not implement io.ReadCloser because it lacks Close. You can wrap a *bytes.Buffer in a struct that adds a no-op Close method to satisfy the interface, or you can use a different interface that matches the capabilities.

Go convention favors small, focused interfaces. io.Reader has one method. io.Writer has one method. Embedding lets you combine them when you need the combination without polluting the single-method interfaces. This keeps the contracts flexible and reusable.

Accept interfaces, return structs. Embed interfaces to compose contracts, not to save typing.

Pitfalls and compiler errors

Interface embedding is simple, but a few edge cases trip up developers. The most common issue is method collisions.

If you embed two interfaces that both define a method with the same name, the compiler checks the signatures. If the signatures are identical, the embedded interface has that single method. If the signatures differ, the compiler rejects the definition.

// A and B both define Foo with the same signature.
type A interface {
    Foo() int
}

type B interface {
    Foo() int
}

// C embeds both. The method set of C includes Foo() int.
// This is valid because the signatures match.
type C interface {
    A
    B
}

If B defined Foo() string instead, the compiler would stop with an error like embedded type B has method Foo with signature string, but A has method Foo with signature int. The method sets cannot merge when signatures conflict.

Another pitfall is confusing interface embedding with struct embedding. They look similar syntactically but do different things. Struct embedding promotes fields and methods for implementation reuse. Interface embedding merges method sets for contract composition.

// Struct embedding promotes methods for reuse.
type Base struct{}
func (Base) Hello() { fmt.Println("hello") }

type Derived struct {
    Base // Embeds Base struct. Derived has Hello method.
}

// Interface embedding merges requirements.
type IBase interface {
    Hello()
}

type IDerived interface {
    IBase // Embeds IBase interface. IDerived requires Hello.
}

If you try to embed a non-interface type in an interface, the compiler rejects it with cannot embed type T. Interfaces can only embed other interfaces.

Goroutine leaks happen when the goroutine waits on a channel that never gets closed. Always have a cancellation path. Interface embedding has no runtime cost, so there is no leak risk from the embedding itself. The risk comes from how you use the values that satisfy the interface.

The worst interface bug is the one that compiles but panics at runtime because the dynamic type lacks a method. The compiler prevents this by checking satisfaction at the assignment site. Trust the compiler.

Decision matrix

Use interface embedding when you want to compose behaviors from smaller, reusable contracts. Use a flat interface with all methods listed when the methods are tightly coupled and do not make sense as separate interfaces. Use struct embedding when you need code reuse and field promotion in an implementation, not in a contract. Use no embedding when the interface is small and simple, as embedding adds cognitive load without benefit for a single method.

Interface embedding is a declaration, not a runtime operation.

Where to go next