arrow_backBACK TO GO FOR BACKEND ENGINEERS — CONCURRENCY, SIMPLICITY AND REAL NUMBERS
Lesson 03Go for Backend Engineers — Concurrency, Simplicity and Real Numbers6 min read

Interfaces & Composition

October 29, 2026

TL;DR

Go has no classes and no inheritance; its structure comes from two ideas. (1) INTERFACES ARE IMPLICIT: a type satisfies an interface simply by having the right methods — there is no 'implements Store' declaration anywhere. The CONSUMER declares the small interface it needs, and any existing type with those methods fits, even types from packages that never heard of your interface. This flips dependency direction and makes code loosely coupled: your order service depends on a tiny Store interface, not on PostgreSQL, so you swap a real DB for an in-memory fake in tests with no mocking framework. Two rules: 'accept interfaces, return structs', and keep interfaces SMALL (io.Reader is one method and half the standard library plugs into it). (2) COMPOSE, DON'T INHERIT: there is no base class; you EMBED small types to reuse their behaviour. A Server HAS-A Logger (embedded), it is not-A Logger. You assemble a type from independent, testable parts instead of a fragile class tower. Small pieces, loosely joined.

Coming from an object-oriented language, your instinct is to reach for classes, inheritance and implements clauses. Go has none of these, and yet large, well-structured Go codebases exist. They get their structure from two ideas that feel strange at first and then feel liberating: implicit interfaces and composition over inheritance. Understanding these two is the difference between writing Java-with-Go-syntax and writing idiomatic Go.

Interfaces Are Implicit

In most languages, a class must declare that it implements an interface: class PgStore implements Store. In Go, there is no such declaration. A type satisfies an interface automatically, just by having the right methods. If a type has the methods an interface lists, it is that interface — nothing needs to be said.

Interfaces are implicit. Define what you NEED: type Store interface with Save ctx Order returns error and Get ctx id returns Order error; small interfaces, often one or two methods. Any type with those methods fits: type PgStore struct holds db; func on PgStore Save returns error; func on PgStore Get; PgStore IS a Store, automatically, with no 'implements Store' anywhere. Why implicit interfaces change how you design: the consumer declares the interface it needs, and any existing type that happens to have those methods satisfies it — including types from packages that never heard of your interface; this flips the usual dependency direction and makes code genuinely loosely coupled. Your order service depends on a tiny Store interface, not on PostgreSQL; swap PgStore for a MemStore in tests, no mocking framework, just a struct with the same methods. Accept interfaces, return structs — take the narrow behaviour you use, hand back the concrete thing. Keep interfaces small; io.Reader is one method Read and half the standard library plugs into it; a big interface is hard to satisfy and hard to fake, a one-method one is used everywhere.

Here is the shape:

// The consumer declares what it needs:
type Store interface {
    Save(ctx context.Context, o Order) error
    Get(ctx context.Context, id int) (Order, error)
}

// Any type with those methods satisfies it — automatically:
type PgStore struct{ db *sql.DB }
func (p PgStore) Save(ctx context.Context, o Order) error { /* ... */ }
func (p PgStore) Get(ctx context.Context, id int) (Order, error) { /* ... */ }
// PgStore IS a Store now. No "implements Store" written anywhere.

This sounds like a small detail. It is actually a profound change in how dependencies flow. The consumer declares the interface it needs, and any type that happens to have those methods satisfies it — including types from packages written long before your interface existed, by people who never heard of it. You do not go to the PgStore definition and add implements; you define the narrow Store interface next to the code that uses it, and PgStore just fits. Dependencies point inward, toward the small interfaces your code defines, rather than outward toward concrete implementations.

The practical payoff is loose coupling and trivial testing:

  • Your order service depends on the tiny Store interface, not on PostgreSQL. It never imports the database package; it just needs something that can Save and Get.
  • In tests, you pass a MemStore — a struct backed by a map with the same two methods. No mocking framework, no code generation, no magic: just a different struct that satisfies the same interface. Swapping the real DB for a fake is a one-line change at the call site.

Two rules make this work in practice:

  • “Accept interfaces, return structs.” Functions should take the narrow interface of behaviour they actually use, and return concrete types. Taking an interface keeps the caller free to pass anything that fits (including a fake); returning a concrete struct gives the caller all the type’s capabilities rather than a hobbled interface view.
  • Keep interfaces small. The best Go interfaces are one or two methods. io.Reader is a single method, Read(p []byte) (int, error), and half the standard library plugs into it — files, network connections, buffers, HTTP bodies, gzip streams all satisfy it, so anything that reads bytes works with anything that consumes them. A big, fat interface is hard to satisfy and hard to fake; a one-method interface is used everywhere. Define interfaces where they are used, not where types are defined.

Compose, Don’t Inherit

The second idea is how you reuse behaviour without inheritance. Go has no base classes, no extends, no super(), no hierarchy of types. Instead you embed one type inside another.

Compose, don't inherit. No inheritance tree: Go has no extends, no base classes, no super(), no deep hierarchies; the classic OOP problem — a change to a base class rippling through dozens of subclasses — simply cannot happen because there is no base class. Embed to compose: type Server struct holds an embedded pointer to log.Logger and a db handle; Server gets Logger's methods for free; it HAS-A logger, it is not-A logger. Has-a over is-a: embedding promotes the embedded type's methods onto the outer type, so you reuse behaviour without a hierarchy; it stays composition — Server HAS-A Logger, it is not pretending to BE a Logger; you assemble a type from small independent parts — a logger here, a DB handle there, a mutex for state — and each part is testable and swappable on its own; small pieces loosely joined beats a tower of classes where touching the bottom shakes the top.
type Server struct {
    *log.Logger          // embedded — Server gets Logger's methods
    db  *sql.DB
    cfg Config
}

srv.Println("starting up")   // works — promoted from the embedded *log.Logger

Embedding a type promotes its methods onto the outer type, so Server can call Println as if it were its own method. You have reused Logger’s behaviour without any hierarchy. Crucially, this stays composition, not inheritance: Server has-a Logger, it is not pretending to be a Logger. There is no “is-a” relationship to reason about, no virtual dispatch down a chain of overrides, no fragile base class.

Why does this matter? The classic inheritance headache — you change a base class and the change ripples, sometimes breaking, through dozens of subclasses you forgot existed — simply cannot happen in Go, because there is no base class. You build a type by assembling small, independent parts: a logger here, a DB handle there, a mutex to guard some state, a config struct. Each part is defined, tested and understood on its own, and you compose them into the bigger thing. Small pieces, loosely joined, beats a tall tower of classes where touching the bottom shakes the top. (This is the same “keep it simple so a team can maintain it” thread from Lesson 1, applied to how you structure code.)

A related idiom you will see constantly: the empty interface interface{} (now also spelled any) is satisfied by every type, since every type has at least zero methods. It is Go’s escape hatch for “any value at all” — useful but to be used sparingly, because you lose all type information and must type-assert to get it back. Reach for a real, small interface first; any is a last resort, not a default.

Putting It Together: A Testable Design

These two ideas combine into the standard way a Go service is structured:

  • Define small interfaces at the point of use for the behaviours a component depends on (Store, Mailer, Cache).
  • Have your components accept those interfaces so real and fake implementations are interchangeable.
  • Compose each component from small embedded or field-held parts rather than extending a base.
  • Wire the concrete implementations together in main (or a small setup function) — this is dependency injection, done by hand with plain values, no framework required.

The result is code that is loosely coupled (components depend on narrow behaviours, not concrete packages), easy to test (pass a fake that satisfies the interface), and free of the inheritance tangles that make large OO codebases hard to change. It is a different shape from what you are used to, but once it clicks, you will find it is less machinery for the same flexibility.

Pitfalls to Avoid

  1. Writing big, fat interfaces. A ten-method interface is hard to satisfy and hard to fake, and it couples callers to a lot of behaviour. Prefer one- or two-method interfaces; split large ones. Remember io.Reader is a single method.
  2. Defining interfaces where the type lives, not where it’s used. Idiomatic Go defines the interface next to the consumer that needs it. Exporting a big interface from the implementing package is the OO habit, not the Go one.
  3. Returning interfaces instead of concrete types. “Accept interfaces, return structs.” Returning an interface hides capabilities the caller may need and adds indirection for no benefit. Hand back the concrete thing.
  4. Reaching for any/interface{} as a default. It throws away type safety and forces type assertions everywhere. Use a real small interface; reserve any for genuine “any value” cases (and prefer generics now where they fit).
  5. Trying to simulate inheritance with embedding. Embedding is has-a composition, not is-a inheritance. Don’t build deep embedding chains to fake a class hierarchy — compose small, independent parts instead.
  6. Over-abstracting too early. Defining interfaces for things with exactly one implementation and no test seam yet. Add the interface when you actually need the seam (a second implementation, or a fake for tests), not speculatively.

Quick Recap

  • Interfaces are implicit: a type satisfies an interface just by having the methods — no implements. The consumer defines the small interface it needs, and any type with those methods fits, flipping dependency direction and making code loosely coupled and trivially testable (pass a fake struct, no mocking framework).
  • Two rules: “accept interfaces, return structs,” and keep interfaces small (one or two methods — io.Reader is one and runs half the stdlib).
  • Compose, don’t inherit: Go has no classes or base types. Embed small types to reuse their methods — Server has-a Logger, it is not-a Logger. The fragile-base-class problem can’t occur because there is no base class.
  • Build types from small, independent, testable parts; wire concretes together in main. Use any sparingly.

Next lesson: Go’s headline feature and the reason it dominates backend infrastructure — goroutines and the scheduler that runs a million of them on a handful of threads.