# Interfaces and Composition

> Go interfaces are satisfied implicitly: small interfaces and embedding replace inheritance, and an adapter behind an interface keeps implementations swappable.

Source: https://sheng.page/en/posts/go-interfaces-and-composition/ · Published: 2025-05-25 · Author: Sheng

Go has no `implements` keyword; interfaces are satisfied implicitly. That makes composition over inheritance natural, and lets gortex swap tracing from in-memory to OTel without touching business code.

## Small interfaces, implicit satisfaction

A type satisfies an interface simply by having its methods — no declaration needed:

```go
type Stringer interface { String() string }

type Point struct{ X, Y int }
func (p Point) String() string { return fmt.Sprintf("(%d,%d)", p.X, p.Y) }
// Point is a Stringer automatically; no "implements" anywhere
```

The convention is to keep interfaces small. The standard library's `io.Reader` and `io.Writer` have one method each, which is why they compose everywhere. Two Go proverbs nail it: "the bigger the interface, the weaker the abstraction", and "accept interfaces, return structs". Take a small interface as a parameter and callers can pass the widest range of things.

## Composition over inheritance: embedding

Go has no inheritance; you compose with embedding plus method promotion:

```go
type Server struct {
    *log.Logger // embedded; Server gets Logger's Printf and friends directly
}

s := &Server{Logger: log.Default()}
s.Printf("up") // promoted from the embedded Logger

type ReadWriter interface { // interfaces embed too
    io.Reader
    io.Writer
}
```

This isn't subclassing — it just promotes the embedded type's method set to the outer type. To override, define a method of the same name on the outer type; there's no `super`.

## Dependency inversion and adapters

Program against a small interface rather than a concrete type and the implementation becomes swappable. An adapter wraps a foreign type to satisfy the interface you want.

Gortex's tracing is the ready example: the framework depends on a single `Tracer` interface, wired by default to an in-memory `SimpleTracer`; to adopt OpenTelemetry you drop in an OTel adapter implementing the same interface, and business code doesn't change a line (see [Observability: Three-State Health Checks and a Tracing Abstraction](/en/posts/gortex-observability-health-tracing/)).

## From Effective Go: Interfaces, Embedding

Effective Go's [Interfaces and methods](https://go.dev/doc/effective_go#interfaces_and_methods) frames an interface as a set of methods, with one type free to satisfy many. [Embedding](https://go.dev/doc/effective_go#embedding) covers interface and struct embedding, how methods are promoted, and why it isn't inheritance. Together they are how Go organises code: composition and small interfaces, not a type hierarchy.


## Notes

- Interfaces are satisfied implicitly: having the methods is enough, no `implements` to declare.
- Small interfaces are the most useful; "the bigger the interface, the weaker the abstraction".
- Embedding is composition plus method promotion, not inheritance; a same-named method on the outer type overrides.
- Program to an interface and add an adapter to keep implementations swappable; gortex's `Tracer` is exactly this (see the observability post).
- Interface-driven design runs through the whole framework; you'll see it throughout the framework posts.

Further reading: the series goes on to dissect the gortex framework built on these ideas — source at [yshengliao/gortex](https://github.com/yshengliao/gortex).

---

> Ideas and technical judgement by Sheng; drafted with Claude · Go 1.24 (latest release at the time) · compiled retroactively · part of the 2026-06-13 blog renovation, paint still drying.