Andrea Cremese

A nerd with an MBA

Go Interfaces: Thoughts on Composition Over Inheritance

Introduction

One of the patterns in Object Oriented Programming to reuse code is inheritance. A design principle is “composition over inheritance”, and there are quite a few videos about that for Go. Most of them focus on struct embedding. But looking at the code AI generates, and at the standard library, there are deeper considerations — a framing to decide “Who should own the contract — the producer or the consumer? And does it depend on context?”

TL;DR

Background — struct embedding (the half every Go tutorial covers)

Go has no class inheritance. Instead, you embed one struct into another — “has-a” rather than “is-a”:

type Auth struct{}

func (a *Auth) GetToken() string {
	return "a token"
}

type User struct {
	Name string
	Auth // embedded — User now has GetToken() for free
}

var u User
u.GetToken() // works, no super() call, no class hierarchy

This is well-covered elsewhere. But it’s only half of Go’s composition story.

The two patterns, side by side

Pattern A: Producer-defined interface (the OOP-flavored approach)

// producer package
type Repository interface {
    GetByID(id string) (*Order, error)
    Save(order *Order) error
}

type postgresRepo struct { db *sql.DB } // unexported — consumers depend on the interface
func (r *postgresRepo) GetByID(id string) (*Order, error) { ... }
func (r *postgresRepo) Save(order *Order) error { ... }
// consumer package — imports the producer's interface
import "myapp/repository"

func NewService(repo repository.Repository) *Service { ... }

Pattern B: Consumer-defined interface (the idiomatic Go approach)

// producer package — exports concrete type only
type PostgresRepo struct { db *sql.DB }
func (r *PostgresRepo) GetByID(id string) (*Order, error) { ... }
func (r *PostgresRepo) Save(order *Order) error { ... }
func (r *PostgresRepo) ListByCustomer(custID string) ([]*Order, error) { ... }
// consumer package — defines only what it needs
// its tests will mock only what it needs, and will not use anything from the producer package really.
type orderFetcher interface {
    GetByID(id string) (*Order, error)
}

func NewService(fetcher orderFetcher) *Service { ... }

// in the tests
type fetcherMock struct {}

func (fm *fetcherMock) GetByID(id string) (*Order, error) {
	// implement behavior as needed, in the consuming package
}

The consumer asks for one method out of three. Interface Segregation Principle for free.

Standard library examples (pinned to Go 1.26.1):

What each pattern optimizes for

Pattern A (producer-defined)

Pattern B (consumer-defined)

The conditional: it depends on where the boundary sits

The right pattern depends on the ownership boundary.

Exported package → Pattern B wins

Encapsulated code that exports an API → Pattern A is often better

Common failure modes (regardless of pattern)

What about the AI code assistants

One nihilist argument I hear is that code is irrelevant in the era of code assistants. As someone pretty close to the forefront of using code assistants in 2026, I still think these patterns matter. Here is why:

In other words:

Being deliberate about which pattern you use where isn’t just good engineering, it’s how you steer the AI tools that now write most of your code.