← Field notes

Clean code · 6 MIN READ

Clean Go without a tower of abstractions

Small consumer interfaces, explicit ownership and useful errors are more valuable than an interface for every struct.

01

Read the call before opening the implementation

A clear API tells the caller what an operation does, what it owns and how it can fail. A method called Process hides the important decision. ReserveStock expresses intent. A duration type is clearer than an integer timeout with no unit.

Good names cannot replace a behavioral contract. A returned slice may share a backing array; an interface with a Get method does not say what missing means or whether concurrent calls are safe. Put those promises in documentation and verify them at the boundary.

02

Let consumers ask for the capability they need

A handler that reads one order should not depend on a twenty-method repository interface. Define a small contract beside the consuming use case. The concrete store satisfies it implicitly, and another consumer can ask for a different view of the same implementation.

Return concrete types from constructors when that is the useful API. A pure calculation rarely needs an interface. Introduce substitution where it makes an IO, time or policy boundary testable, rather than adding a mockable wrapper around every helper.

03

Carry an error to the layer that can act

Add operation context while preserving a cause only when that cause is part of your public contract. Use errors.Is or errors.As for documented categories; do not parse human messages. Map storage errors into application meaning when driver details should remain private.

Log at the boundary that handles the failure. Logging the same error at repository, service and transport layers turns one outage into three messages. Retries need a known idempotency policy and one total deadline. A low-level helper should not retry every error just because it can.

04

Own mutable data and resource lifetimes

Returning a slice copies a descriptor, not the elements. A shallow clone of pointers still shares the pointed-to objects. Choose borrowed data, an immutable snapshot or caller-owned data deliberately. When a lock protects a map, the slices retrieved from that map also need an ownership rule after the lock is released.

The code that acquires a resource should define who closes it. The owner of a channel closes it after producers finish; receivers use cancellation rather than closing an arbitrary shared channel. A goroutine needs a stop condition and an observable completion boundary.

05

Refactor one seam at a time

Capture behavior around the seam, move one responsibility without changing results, then make the behavior change separately. Keep compatibility for durable schemas, queued events and external callers. For a write migration, choose one authoritative path rather than running old and new effects in parallel.

A regression test should describe the user-visible defect or contract. Tests that only repeat the implementation make a folder move expensive without proving the system still works. Use fakes to test application decisions and focused real-adapter tests to verify transactions, uniqueness and mapping.

Sources & further reading

← All field notes