Functions and Interfaces: Go's Composition Model
Go shares behavior through functions, methods, and interfaces instead of class inheritance.
Search across all documentation pages
Go shares behavior through functions, methods, and interfaces instead of class inheritance.
That design keeps types small, dependencies explicit, and APIs easy to test without deep framework magic.
Traditional object-oriented languages often model "is-a" relationships with inheritance: a HTTPServer is a Server, which is a Listener.
Go deliberately omits that mechanism.
Instead, a type holds data (struct fields) and methods attach behavior via receivers.
When two types need shared behavior, you extract a function or define a tiny interface that both satisfy.
Interfaces in Go are sets of method signatures.
A concrete type implements an interface automatically when it has the required methods - no implements keyword, no code generation step.
That keeps producers decoupled from consumers: the HTTP handler package defines type Handler interface { ServeHTTP(...) }; your struct just adds the method.
Embedding (anonymous struct or interface fields) is Go's other composition lever.
Embedding promotes methods onto the outer type, similar to delegation, but it is not inheritance - there is no substitutability guarantee beyond the promoted method set.
A useful analogy: Go types are LEGO bricks with studs (methods); interfaces are the shape of the hole you need.
Any brick with matching studs fits, regardless of color or internal structure.
Control flow in Go services typically layers like this:
┌──────────────┐ depends on ┌──────────────┐
│ handler │ ────────────────► │ interface │
│ (concrete) │ │ (consumer) │
└──────┬───────┘ └──────▲───────┘
│ calls │ satisfied by
▼ │
┌──────────────┐ ┌──────┴───────┐
│ service │ │ repository │
│ struct │ │ mock / pg │
└──────────────┘ └──────────────┘
Functions are first-class values: you can pass them, return them, and store them in variables.
That enables middleware chains (func(http.Handler) http.Handler), retry policies, and test doubles without interface boilerplate when a single function suffices.
Methods bind behavior to a type and participate in method sets.
The set differs for pointer vs value receivers, which affects interface satisfaction - a detail that bites teams who mix receiver styles on one type.
Interfaces are satisfied implicitly at compile time.
The compiler checks assignments; runtime uses itable lookups for non-empty interfaces (type + data word pair).
Empty interface interface{} (or any) holds any value but sacrifices static checking - prefer typed interfaces at boundaries.
The idiomatic phrase "accept interfaces, return structs" encodes composition discipline: callers depend on the smallest behavior they need; constructors return concrete types so callers are not locked into your interface shape.
// Consumer defines the contract.
type Clock interface {
Now() time.Time
}
// Producer returns concrete type; tests swap via interface parameter.
type RealClock struct{}
func (RealClock) Now() time.Time { return time.Now() }Large Go codebases stay healthy when composition rules are enforced in review, not left to intuition.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
| Small consumer interfaces | Easy fakes, stable mocks | Many named interfaces | Libraries, domain services |
Function parameters (func(...)) | Minimal ceremony | Harder to compose multi-method APIs | Middleware, hooks |
| Embedding concrete structs | Rapid reuse of defaults | Hidden promoted API surface | http.Server wrappers, decorators |
| Generics with constraints | Shared algorithms | Can obscure intent if overused | Collections, parsers |
any at boundaries | Interop with dynamic data | Loses compile-time safety | JSON decode staging only |
gRPC and HTTP frameworks (chi, gin, echo) compose via functions and interface-shaped middleware.
Your business packages should still expose domain interfaces, not framework types, so swapping routers does not rewrite core logic.
Kubernetes controller-runtime reconcilers compose through interfaces (client.Client, reconciler.Reconciler) while structs embed client.Client for delegation - a canonical large-scale composition pattern.
Testing replaces interfaces with fakes recording calls.
If an interface grows past ~3 methods, split it or pass functions per method group.
Versioning: exported interfaces are compatibility commitments.
Adding methods breaks implementers; prefer new interface names (Reader + ReadCloser) or unexported interfaces satisfied by exported structs.
== nil; this breaks many if x == nil guards.It avoids fragile base-class hierarchies and hidden overrides.
Teams compose behavior explicitly with functions, small interfaces, and embedding so changes stay local and testable.
A struct stores data.
An interface describes behavior (method signatures) and holds a runtime type plus value pair when assigned.
Interfaces do not carry fields of their own.
Use methods when behavior is tied to a type's lifecycle or when you need a method set for interfaces.
Use package-level functions when the operation is generic and does not need receiver state.
Function parameters should be the smallest interface callers need.
Return concrete structs (or named types) so users are not forced to depend on your interface definitions.
Anonymous embedded fields promote methods to the outer type.
You reuse implementation without subclassing, but exported promoted methods become part of your public API - embed deliberately.
Decoupling: producers should not import consumer packages just to declare satisfaction.
The compiler verifies method sets at assignment sites instead.
Method sets include value methods for both T and *T, but only pointer methods on *T.
Interface satisfaction depends on which methods are required and which receiver type you assign.
Type parameters can be constrained by interfaces (interface { ~int | ~int64 } or standard constraints).
Use generics for algorithms shared across types; use interfaces for runtime polymorphism and I/O boundaries.
Not always - it is appropriate at decoding boundaries.
Avoid any in domain APIs where a typed interface or struct communicates intent to readers and the compiler.
Handlers are functions or types with ServeHTTP.
Compose cross-cutting concerns with func(http.Handler) http.Handler middleware rather than base handler classes.
Exporting wide interfaces from producer packages.
That locks consumers to large mocks and makes API evolution painful.
main wires concrete servers and passes context.Context through interfaces.
Composition keeps shutdown logic in one place instead of scattered override chains.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 18, 2026