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.
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
Storeinterface, not on PostgreSQL. It never imports the database package; it just needs something that canSaveandGet. - 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.Readeris 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.
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.LoggerEmbedding 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
- 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.Readeris a single method. - 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.
- 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.
- Reaching for
any/interface{}as a default. It throws away type safety and forces type assertions everywhere. Use a real small interface; reserveanyfor genuine “any value” cases (and prefer generics now where they fit). - 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.
- 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.Readeris one and runs half the stdlib). - Compose, don’t inherit: Go has no classes or base types. Embed small types to reuse their methods —
Serverhas-aLogger, it is not-aLogger. 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. Useanysparingly.
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.