How to Compose Interfaces in Go (Interface Embedding)

Compose Go interfaces by embedding existing interfaces to inherit their methods and create new, combined contracts.

Composing interfaces with embedding

You are building a data pipeline. One component reads configuration from a file. Another writes logs to standard output. A third component needs to do both: read input, process it, and write the result. You already have a Reader interface for the first component and a Writer interface for the second. Defining a new Processor interface that lists Read and Write methods feels redundant. Copying signatures duplicates code and creates drift. If Reader gains a new method later, Processor won't update automatically.

Go solves this with interface embedding. You can place one interface inside another. The outer interface inherits every method from the inner one. The result is a single interface that requires all the methods from all the embedded pieces. This keeps contracts DRY and ensures consistency across your codebase.

The concept: merging contracts

Interface embedding merges method sets. When you embed Reader inside ReadWriter, the ReadWriter interface requires the Read method. If you also embed Writer, it requires Write as well. The embedded interface disappears from the method list; its methods become part of the outer interface.

Think of interface embedding like combining skill badges. A Reader badge proves you can read. A Writer badge proves you can write. A ReadWriter badge is a card that holds both badges. If you show the ReadWriter card, the guard knows you can read and write. If you show just the Reader badge, the guard knows you can read. The ReadWriter card is valid anywhere a Reader badge is accepted because it contains the Reader badge inside it.

The compiler treats the embedded interface as part of the definition. There is no runtime cost for embedding. Embedding is purely a compile-time mechanism for grouping method requirements.

Minimal example

Here is the simplest composition: embed two small interfaces to create a combined contract.

package main

import (
	"fmt"
)

// Reader defines the contract for pulling data.
type Reader interface {
	Read(p []byte) (n int, err error)
}

// Writer defines the contract for pushing data.
type Writer interface {
	Write(p []byte) (n int, err error)
}

// ReadWriter embeds Reader and Writer to require both capabilities.
// The method set includes Read and Write without listing them explicitly.
type ReadWriter interface {
	Reader
	Writer
}

// File implements Read and Write, so it satisfies ReadWriter automatically.
type File struct {
	name string
}

// Read implements Reader.
func (f *File) Read(p []byte) (int, error) {
	return 0, nil
}

// Write implements Writer.
func (f *File) Write(p []byte) (int, error) {
	return len(p), nil
}

func main() {
	f := &File{name: "data.txt"}

	// f satisfies ReadWriter because it has both Read and Write.
	var rw ReadWriter = f

	// f also satisfies Reader alone.
	var r Reader = f

	fmt.Printf("ReadWriter: %T\n", rw)
	fmt.Printf("Reader: %T\n", r)
}

The File struct implements Read and Write. It satisfies ReadWriter because ReadWriter requires exactly those methods. It also satisfies Reader and Writer individually. Go interfaces are structural. The compiler checks the method set, not the name. If the methods match, the type fits.

Go developers follow a pattern: accept interfaces, return structs. When you compose interfaces, you are refining what a function accepts. You might return a *File struct, but the function signature asks for a ReadWriter. This keeps your code flexible and testable.

How the compiler handles embedding

When the compiler encounters an embedded interface, it builds a merged method set. It looks at Reader, finds Read. It looks at Writer, finds Write. The method set for ReadWriter becomes {Read, Write}.

The compiler generates a type descriptor for ReadWriter that includes both methods. Any type that has both methods in its method set satisfies ReadWriter. The check is identical to checking a flat interface with all methods listed. Embedding does not add overhead.

If you embed an interface multiple times, the compiler merges the methods. If the same method appears in multiple embedded interfaces, the signatures must match. If they differ, the compiler rejects the code. This prevents ambiguous behavior where a single method call could resolve to different implementations.

Convention aside: gofmt formats embedded interfaces consistently. The embedded interface name stands alone on its own line inside the interface block. Don't fight the formatter. Trust gofmt to align the code. Most editors run it on save.

Realistic example: a storage layer

Here is a realistic scenario: a storage backend that supports getting and setting values. You separate the concerns into Getter and Setter, then compose them into a Store.

package main

import (
	"fmt"
)

// Getter defines retrieval of data by key.
type Getter interface {
	Get(key string) (string, error)
}

// Setter defines storage of data by key.
type Setter interface {
	Set(key string, value string) error
}

// Store embeds Getter and Setter to require full read-write access.
// Functions accepting Store can read and write without checking capabilities.
type Store interface {
	Getter
	Setter
}

// MemoryStore implements both Getter and Setter.
type MemoryStore struct {
	data map[string]string
}

// Get implements Getter.
func (m *MemoryStore) Get(key string) (string, error) {
	val, ok := m.data[key]
	if !ok {
		return "", fmt.Errorf("key not found: %s", key)
	}
	return val, nil
}

// Set implements Setter.
func (m *MemoryStore) Set(key string, value string) error {
	m.data[key] = value
	return nil
}

// Sync reads a value, transforms it, and writes it back.
// It accepts Store to ensure the backend supports both operations.
func Sync(s Store, key string) error {
	val, err := s.Get(key)
	if err != nil {
		return err
	}

	// Transform the value.
	newVal := val + "_synced"

	return s.Set(key, newVal)
}

func main() {
	store := &MemoryStore{data: map[string]string{"config": "initial"}}

	// store satisfies Store because it implements Get and Set.
	err := Sync(store, "config")
	if err != nil {
		fmt.Println("Error:", err)
	}

	val, _ := store.Get("config")
	fmt.Println("Result:", val)
}

The Sync function accepts a Store. It doesn't care if the underlying type is MemoryStore, RedisStore, or FileStore. It only cares that the type implements Get and Set. Embedding Getter and Setter into Store makes this contract explicit and reusable.

Convention aside: receiver names should be one or two letters matching the type. Use (m *MemoryStore) not (this *MemoryStore) or (self *MemoryStore). This keeps method signatures concise and matches the standard library style.

Pitfalls and compiler errors

Interface embedding introduces a few traps. The most common is method collision. If two embedded interfaces define a method with the same name but different signatures, the compiler stops you.

// Bad: Close has different signatures in CloserA and CloserB.
type CloserA interface {
	Close() error
}

type CloserB interface {
	Close(timeout int) error
}

// This fails to compile.
type BadEmbed interface {
	CloserA
	CloserB
}

The compiler rejects this with method Close has two different signatures. The merged method set cannot contain two methods named Close with different parameters. You must resolve the conflict by renaming one method or restructuring the interfaces.

Another pitfall is confusing interface embedding with struct embedding. They look similar in syntax but serve different purposes. Struct embedding promotes methods from the embedded field. Interface embedding merges method sets.

// Struct embedding promotes methods.
type Base struct{}

func (b *Base) Hello() {
	fmt.Println("Hello")
}

type Derived struct {
	Base // Embeds Base struct.
}

// Derived inherits Hello from Base automatically.
d := &Derived{}
d.Hello() // Prints Hello.

Struct embedding is for code reuse. Interface embedding is for type composition. If you embed a struct, you get the fields and methods of the embedded type. If you embed an interface, you only get the method requirements. You cannot embed a struct inside an interface. You can only embed interfaces inside interfaces.

The compiler complains with cannot embed type T if you try to embed a struct in an interface. Stick to interfaces for composition. Use struct embedding when you need to reuse implementation logic.

Goroutine leaks can happen when interfaces hide cancellation paths. If an interface method blocks indefinitely and the caller has no way to cancel, the goroutine leaks. Always design interfaces that respect context.Context. Functions that take a context should pass it through to long-running operations.

Convention aside: context.Context always goes as the first parameter, conventionally named ctx. Interfaces that represent operations should include context in their methods. This allows callers to enforce deadlines and cancellation.

Interface embedding vs struct embedding

The distinction matters. Struct embedding creates a relationship between types. If Derived embeds Base, Derived has a Base field. You can access Base methods through Derived. You can also access Base fields if they are exported.

Interface embedding creates a relationship between contracts. If Store embeds Getter, Store requires Get. A type satisfying Store must have Get. There is no field relationship. The type just needs the methods.

Use interface embedding to build complex contracts from simple pieces. Use struct embedding to reuse code. They solve different problems. Mixing them up leads to confusion about what the compiler is doing.

At runtime, an interface value is a pair: a pointer to the type information and a pointer to the data. When you embed interfaces, the type information changes to include the merged method set. The cost of checking the interface remains the same. The compiler generates a type descriptor that lists all methods. Embedding is a compile-time operation. There is no runtime penalty for embedding.

Decision matrix

Use interface embedding when you want to combine existing contracts without duplicating method signatures. Use a single interface with all methods listed when the interface is small and self-contained, avoiding the mental overhead of looking up embedded definitions. Use struct embedding when you need to reuse implementation logic, not just the type contract. Use a new interface with renamed methods when the combined behavior changes the semantic meaning of the operations. Use the empty interface any when you need to accept any type, but prefer specific interfaces to preserve type safety.

Interfaces are contracts. Embedding is just shorthand for the contract terms. Compose interfaces to reduce duplication. Don't compose them to create god-interfaces that require fifty methods. Keep interfaces small and focused. The compiler checks the method set, not the name. If the methods match, the type fits.

Where to go next