Skip to content

Embedding Interfaces — Senior Level

Composition Strategy

Granularity principle

// Atomic
type Reader interface { Read([]byte) (int, error) }
type Writer interface { Write([]byte) (int, error) }
type Closer interface { Close() error }

// Composed
type ReadCloser interface { Reader; Closer }
type ReadWriter interface { Reader; Writer }
type ReadWriteCloser interface { Reader; Writer; Closer }

The caller should ask for the smallest interface possible. io.Copy needs Reader and WriterCloser is unnecessary.

Anti-pattern: kitchen sink interface

// BAD
type Storage interface {
    Read(...) ...
    Write(...) ...
    Close() error
    Stats() Stats
    Backup(...) ...
    Restore(...) ...
    Verify(...) ...
}

ISP is violated. Split into smaller interfaces.

Migration anti → granular

// v1
type FullStore interface { /* 10 methods */ }

// v2 — granular
type Reader interface { ... }
type Writer interface { ... }
type Deleter interface { ... }

// Caller demands only the interface it needs
func ReadOnly(r Reader) { ... }
func WriteOnly(w Writer) { ... }
func ReadWrite(rw interface { Reader; Writer }) { ... }

Decorator Pattern

Simple decorator

type Logger interface { Log(string) }

type ConsoleLogger struct{}
func (ConsoleLogger) Log(msg string) { fmt.Println(msg) }

type TimestampLogger struct{ Logger }
func (t TimestampLogger) Log(msg string) {
    t.Logger.Log(time.Now().Format(time.RFC3339) + " " + msg)
}

// Use
l := TimestampLogger{Logger: ConsoleLogger{}}
l.Log("hi")  // 2026-05-05T12:00:00Z hi

Multi-decorator

type LevelLogger struct{ Logger; level string }
func (l LevelLogger) Log(msg string) {
    l.Logger.Log("[" + l.level + "] " + msg)
}

// Compose
l := LevelLogger{
    Logger: TimestampLogger{Logger: ConsoleLogger{}},
    level:  "INFO",
}
l.Log("hi")  // [INFO] 2026-05-05T12:00:00Z hi

Refactoring with Embedding

Adding a method to existing interface

Breaking — implementations are broken.

// v1
type Reader interface { Read(...) ... }

// v2 — new method
type Reader interface {
    Read(...) ...
    Available() int   // NEW
}

Soft migration — new interface

// v1 keeps
type Reader interface { Read(...) ... }

// v2 — separate interface
type AvailableReader interface {
    Reader
    Available() int
}

Callers can use AvailableReader. The original Reader is unchanged.


Embed for Generics Constraint

type Sortable interface {
    int | int64 | float64 | string
}

type Lengthy interface {
    Len() int
}

type SortableLen interface {
    Sortable
    Lengthy
}

// constraint
func Foo[T SortableLen](xs []T) { ... }

(This is a generics constraint, not a classic interface — it works with type sets.)


Best Practices for Library Authors

1. Public interfaces are atomic

The foundation of a library is atomic interfaces. Let the caller compose, or provide a few typical compositions in your package.

// Public
type Reader interface { ... }
type Writer interface { ... }
type Closer interface { ... }

// Convenience composition
type ReadWriteCloser interface { Reader; Writer; Closer }

2. Short, focused interfaces

The -er suffix on interface names is the Go convention: - Reader, Writer, Closer - Stringer, Marshaler, Validator

3. Documenting the contract

// Reader is the interface that wraps the basic Read method.
//
// Read reads up to len(p) bytes into p. ...
type Reader interface { ... }

// ReadCloser combines Reader and Closer.
//
// Implementations must guarantee that calls to Close are idempotent.
type ReadCloser interface {
    Reader
    Closer
}

Anti-patterns

1. Big embedding chain

type A interface { ... }
type B interface { A; ... }
type C interface { B; ... }
type D interface { C; ... }
type E interface { D; ... }

The hierarchy is too deep — refactoring becomes hard.

2. Unrelated embed

type Logger interface { Log(string) }
type Counter interface { Count() int }

type Bizarre interface { Logger; Counter }

There is no logical relationship.

3. Ignoring method conflict

type A interface { M() string }
type B interface { M() int }
type AB interface { A; B }   // compile error — be careful

Cheat Sheet

COMPOSITION STRATEGY
────────────────────────
Atomic interface first
Build larger via composition
Caller asks for the minimum

DECORATOR
────────────────────────
type X struct { Logger }
func (x X) Log(msg) { x.Logger.Log(modify(msg)) }

LIBRARY DESIGN
────────────────────────
Granular interfaces
Convenience compositions
-er suffix names
Documented contract

ANTI-PATTERNS
────────────────────────
Big chain (5+ levels)
Unrelated embed
Method conflict ignored

REFACTORING
────────────────────────
Adding a new method → BREAKING
Creating a new interface → soft

Summary

Embedding at the senior level: - Granularity — atomic interfaces are preferred - Decorator pattern — embed + override - Soft migration — add a new interface - Library design — atomic + convenience - Anti-patterns — big chain, unrelated embeds

Embedding is the Go-style way to build interfaces through composition. Used correctly, it produces powerful, modular abstractions.