Clean code in Go · 3 MIN READ
Small interfaces belong near the consumer
Describe a real substitution boundary without wrapping every concrete type.
Describe a real substitution boundary without wrapping every concrete type. A consumer knows which behavior it needs. Trace the worked Go example, then compare the benefit with the cost before applying this design to your own project.
Clean Go · A MAP OF THE IDEA
- Consumer asks for Read
- Concrete reader satisfies the contract
- Consumer handles value and error
Useful when
- A boundary needs substitution
- Tests can use small deterministic implementations
Watch out
- Wide interfaces couple unrelated behavior
- An interface containing a nil pointer is not nil
Implementation & operation cost: Low
The design decision
A consumer knows which behavior it needs. Define a small interface there and let concrete implementations satisfy it implicitly. Returning a concrete type often gives callers a clearer API; an interface becomes useful when a caller needs substitution, testing or multiple implementations.
A complete Go example
Read this program from the entry point and follow the calls shown in the diagram. The output is included at the end so you can check the behavior without a compiler.
package main
import (
"fmt"
"io"
"strings"
)
func FirstByte(r io.Reader) (byte, error) {
var b [1]byte
_, err := io.ReadFull(r, b[:])
return b[0], err
}
func main() {
b, err := FirstByte(strings.NewReader("Go"))
fmt.Printf("%c %v\n", b, err)
}
// Output:
// G <nil>
FirstByte accepts only the Read behavior that io.ReadFull needs, so a string reader works without an inheritance relationship. The error still belongs to the result: an empty reader would return EOF, not a valid zero byte.
Tradeoffs and the simpler alternative
Do not define a giant interface beside an implementation merely to mock it. Avoid typed nil pointers inside interface results, and test empty input as well as the successful path. For a local helper with one implementation, a concrete parameter may be clearer.
Let the consumer choose the minimum capability
A handler needing Get(ctx, id) does not need a repository interface with twenty unrelated methods. Define the interface near that handler or application service, and let the concrete store satisfy it implicitly. This avoids coupling clients to methods they never call. Return concrete implementations from constructors unless an abstraction is useful to the caller. Accepting an interface is not inherently better when a function simply needs a byte slice or one callback.
Document behavior that method sets cannot express
The type system checks signatures, not semantics. State whether a missing record is an error, whether results alias internal data, whether the value is concurrency-safe, and whether cancellation can interrupt IO. A typed nil pointer stored in an interface is not a nil interface; avoid returning it as a successful dependency. Tests should verify the same contract for fake and real implementations rather than treating a fake as proof that storage behaves correctly.
Introduce substitution where change is real
Clock, payment and durable storage boundaries often benefit from a small substitute in a test. A pure arithmetic helper rarely needs its own interface. Keep a compile-time implementation check near an adapter when it helps detect drift, and exercise the adapter contract as well. If every interface has exactly one consumer-independent clone of a large concrete method set, reconsider whether the abstraction is protecting a real boundary or only adding navigation.
TAKE THIS WITH YOU
States the behavior needed by its caller. Do not define a giant interface beside an implementation merely to mock it. Avoid typed nil pointers inside interface results, and test empty input as well as the successful path. For a local helper with one implementation, a concrete parameter may be clearer.
Check your understanding
Which statement best describes Small interfaces belong near the consumer?
Go to the source
Progress stays in this browser. No account needed.