4 min read
On this page

Composition Over Inheritance

Go has no classes and no inheritance. Instead of building deep type hierarchies, Go uses struct embedding and interface composition to share behavior. This design choice eliminates entire categories of problems — no diamond inheritance, no fragile base class issues, no confusion about which method gets called. Composition produces flatter, more explicit code.

No Classes, No Inheritance

In languages like Java or Python, you might write:

Animal (base class)
  -> Dog (subclass)
    -> GuideDog (subclass)

In Go, this hierarchy does not exist. There is no extends keyword. Types are not related by a parent-child tree. Instead, you build up behavior by combining small pieces.

Struct Embedding

Embedding places one struct inside another, promoting the inner struct's fields and methods to the outer struct.

type Address struct {
    Street string
    City   string
    State  string
}

func (a Address) FullAddress() string {
    return a.Street + ", " + a.City + ", " + a.State
}

type Employee struct {
    Name string
    Address
}

func main() {
    e := Employee{
        Name: "Alice",
        Address: Address{
            Street: "123 Main St",
            City:   "Portland",
            State:  "OR",
        },
    }

    // Address methods are promoted to Employee
    fmt.Println(e.FullAddress())

    // Fields are also promoted
    fmt.Println(e.City)
}
123 Main St, Portland, OR
Portland

The Employee type does not inherit from Address. It contains an Address and the compiler promotes its fields and methods for convenience.

Promotion Rules

When a struct embeds another type, the embedded type's exported fields and methods are accessible directly on the outer type. The key rules are:

type Base struct {
    ID int
}

func (b Base) Describe() string {
    return fmt.Sprintf("ID: %d", b.ID)
}

type Extended struct {
    Base
    Label string
}

func main() {
    e := Extended{Base: Base{ID: 42}, Label: "test"}

    // Promoted method
    fmt.Println(e.Describe())

    // Promoted field
    fmt.Println(e.ID)

    // You can still access the embedded struct directly
    fmt.Println(e.Base.ID)
}
ID: 42
42
42

If the outer struct defines a method with the same name as the embedded type, the outer method takes precedence.

type Extended struct {
    Base
    Label string
}

func (e Extended) Describe() string {
    return fmt.Sprintf("Extended: %s (%s)", e.Label, e.Base.Describe())
}

func main() {
    e := Extended{Base: Base{ID: 42}, Label: "test"}
    fmt.Println(e.Describe())
}
Extended: test (ID: 42)

If two embedded types have a method with the same name, and the outer struct does not define its own, the compiler reports an ambiguity error at the call site.

Interface Embedding

Interfaces can embed other interfaces to compose larger contracts from smaller ones.

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

type ReadCloser interface {
    Reader
    Closer
}

This is how the standard library builds up io.ReadWriter, io.ReadCloser, and io.ReadWriteCloser from single-method interfaces.

You can also embed interfaces in structs. This is useful for partial implementation or test doubles.

type MockDB struct {
    io.ReadCloser // embed the interface
    records []string
}

Any MockDB value must have its ReadCloser field set (or the methods will panic on nil). This technique is more commonly used in testing to satisfy an interface while only implementing the methods you actually call.

Why Composition Wins

Flatter Hierarchies

Inheritance creates deep trees. Composition creates flat structures. When you read a Go struct, you see exactly what it contains — no need to trace up through parent classes to understand its behavior.

// Everything this type can do is visible right here
type OrderService struct {
    repo   OrderRepository
    mailer EmailSender
    logger *slog.Logger
}

No Diamond Problem

In languages with multiple inheritance, the diamond problem creates ambiguity about which parent's method to call. Go does not have this problem because there is no inheritance. Embedding is explicit, and ambiguities are caught at compile time.

Easier to Understand

With inheritance, calling a method might execute code defined three levels up in the hierarchy. With composition, the code path is direct. If OrderService calls s.repo.Save(order), you know exactly where Save lives.

Flexible Combination

You can embed any number of types and combine any set of interfaces. There are no restrictions about single vs multiple inheritance.

type Server struct {
    *http.ServeMux
    *slog.Logger
    config Config
    db     *sql.DB
}

Designing with Small Interfaces & Struct Embedding

The Go design pattern combines small interfaces with struct embedding to build flexible, testable systems.

// Small, focused interfaces
type UserFinder interface {
    FindUser(id int) (User, error)
}

type UserSaver interface {
    SaveUser(user User) error
}

type UserDeleter interface {
    DeleteUser(id int) error
}

// Compose when you need multiple capabilities
type UserStore interface {
    UserFinder
    UserSaver
    UserDeleter
}

// A concrete implementation satisfies all three
type PostgresUserStore struct {
    db *sql.DB
}

func (s *PostgresUserStore) FindUser(id int) (User, error) {
    var u User
    err := s.db.QueryRow("SELECT id, name FROM users WHERE id = $1", id).
        Scan(&u.ID, &u.Name)
    return u, err
}

func (s *PostgresUserStore) SaveUser(user User) error {
    _, err := s.db.Exec("INSERT INTO users (id, name) VALUES ($1, $2)",
        user.ID, user.Name)
    return err
}

func (s *PostgresUserStore) DeleteUser(id int) error {
    _, err := s.db.Exec("DELETE FROM users WHERE id = $1", id)
    return err
}

Functions that only need to find users accept UserFinder. Functions that only need to save accept UserSaver. Only functions that need everything accept UserStore.

// This function only needs to find users — its dependency is minimal
func GetUserProfile(finder UserFinder, id int) (Profile, error) {
    user, err := finder.FindUser(id)
    if err != nil {
        return Profile{}, err
    }
    return Profile{Name: user.Name}, nil
}

This makes testing straightforward. You implement only the interface you need.

type mockFinder struct {
    user User
    err  error
}

func (m *mockFinder) FindUser(id int) (User, error) {
    return m.user, m.err
}

func TestGetUserProfile(t *testing.T) {
    finder := &mockFinder{user: User{ID: 1, Name: "Alice"}}
    profile, err := GetUserProfile(finder, 1)
    if err != nil {
        t.Fatal(err)
    }
    if profile.Name != "Alice" {
        t.Errorf("got %q, want %q", profile.Name, "Alice")
    }
}

Common Pitfalls

  • Embedding for code reuse alone. Embedding promotes all exported methods, not just the ones you want. If you only need one method from the embedded type, use a named field and call the method explicitly.
  • Confusing embedding with inheritance. An Employee that embeds Address is not a subtype of Address. You cannot pass an Employee to a function expecting an Address.
  • Accidentally exposing methods. Embedding a type promotes all of its exported methods. If the embedded type has methods you do not want on the outer type, use a named field instead.
  • Nil embedded interfaces. Embedding an interface in a struct means calling an unset method will panic. Only use this pattern deliberately, typically in tests.
  • Deep embedding chains. Embedding a struct that itself embeds another struct creates promotion chains that are hard to follow. Keep embedding shallow.

Key Takeaways

  • Go has no classes and no inheritance — use composition instead.
  • Struct embedding promotes fields and methods from the inner type to the outer type.
  • The outer type's own methods take precedence over promoted methods.
  • Interface embedding composes larger interfaces from smaller ones.
  • Small interfaces (1-2 methods) are the building blocks of flexible Go designs.
  • Composition produces flatter, more explicit, and more testable code than inheritance hierarchies.
  • Accept small interfaces, embed structs for reuse, and keep your type relationships shallow.