Frontend Labs
⋯

54. Designing APIs: the Go proverbs, applied

54.1 "Accept interfaces, return structs"

A function that accepts a narrow interface (io.Reader, not *os.File) works with anything satisfying that shape; a function that returns a concrete struct gives callers full access to every field and method, instead of forcing them through whatever subset an interface happened to expose. Returning an interface is occasionally right (when you deliberately want to hide the concrete type, e.g. a constructor for an internal implementation detail) but should be the exception, not the default.

54.2 "The bigger the interface, the weaker the abstraction"

A one-or-two-method interface (io.Reader, Storage.Get/Set) can be satisfied by almost anything, which is exactly what makes it reusable. A ten-method interface can usually only be satisfied by one real implementation and a hand-written mock — at which point it isn't really abstracting anything, just adding indirection.

54.3 "Don't just check errors, handle them gracefully"

// Weak: propagates without adding anything a caller couldn't already see
if err != nil {
	return err
}

// Better: adds context that helps whoever reads this error three layers up
if err != nil {
	return fmt.Errorf("loading config from %s: %w", path, err)
}

54.4 Avoid premature abstraction

A single interface with one implementation and no second implementation in sight is usually just ceremony — Go's implicit interface satisfaction (Chapter 9.1) means you can introduce the interface later, exactly when a second implementation (or a test double) actually shows up, with zero cost paid up front. This mirrors the general engineering principle: three similar concrete structs are better than one premature interface built to anticipate a variation that may never arrive.

54.5 Real-world example

The Storage interface from Chapter 9 earns its keep specifically because it does get a second implementation (CachedStorage wrapping it) — that's the signal an interface was the right call, retroactively. If it had stayed at one implementation forever, a plain *MemStorage struct with no interface would have been equally correct and slightly simpler.


Generated from real-life-golang-guide.md · 8 Oct 2026 16:36