all_lessons/Go/16 · Idiomatic Golesson 17 / 18

Idiomatic Go: the design philosophy as a working tool

Fifteen lessons taught you what Go can express. This one asks the question that actually decides whether code survives a team: of the many ways the language permits, which one should you write? "Idiomatic" is the name for that answer — and in Go it is not folklore or taste-policing. It is the bargain from Lesson 00 applied at the granularity of a function signature: choose the form that costs the next reader the least, and let the tools and the community remove the argument about everything else. By the end you can read a code review comment that just says "not idiomatic" and know exactly what cost it is pointing at.

The thesis, here
Every prior lesson made a feature available; this one is about restraint — which available thing to use, and which to refuse. That is the bargain (complexity must be earned) operating not on the language designers' desk but on yours, line by line. Idiomatic Go is what "the cost to the reader and the team" looks like when it is small enough to fit in a single naming decision, a single return signature, a single choice between an interface and a struct. The proverbs you'll meet are not rules; they are compressed reasoning about that cost, and the transferable habit is learning to feel the cost before someone flags it in review.
Linear position
Prerequisite: Lesson 15 — you have the tooling (gofmt, go vet, go test, the race detector) that mechanically enforces a baseline, and Lessons 05–07 and 12–13 gave you interfaces, errors, and packages, which is where most idiom decisions actually live.
New capability: defend or critique a Go API design out loud in the language the community uses — "accept interfaces, return structs," "a little copying," "the zero value should be useful" — and explain the reader-cost each proverb is protecting against, rather than reciting it.
The plan
Four moves. (1) Define "idiomatic" precisely — not style, but the lowest-reader-cost form among equivalents — and show the two-layer machine (mechanical + social) that enforces it. (2) Walk the proverbs that carry real engineering weight, each tied to the cost it removes. (3) Turn it into concrete API-design rules: naming, signatures, interface placement, error shape, the useful zero value. (4) Mark the line between idiom and dogma, because cargo-culting a proverb is itself unearned complexity.

1 · What "idiomatic" actually means

The language permits many spellings of the same intent. You can return (T, error) or stash the error on a struct field; you can take an interface parameter or a concrete type; you can name a variable i or theCurrentLoopIndex. The compiler accepts all of them. "Idiomatic" is the claim that among forms the compiler treats as equal, the team does not: one of them costs the next reader less, and that one is the idiom. It is the bargain's half-one — complexity must be earned — pushed down to the smallest scale, where the unit of complexity is a reader's moment of hesitation.

This is why "not idiomatic" is a real review comment and not snobbery. It means: you wrote a form that works but makes the reader stop and reconstruct intent the standard form would have made obvious. The cost is invisible to you, the writer, who already knows what you meant — exactly the asymmetry Lesson 00 named. Idiom is the team's shared compression: a vocabulary of expected shapes, so a reader spends attention on what is unusual about your code instead of decoding what should have been routine.

Go enforces this in two layers, and the split is itself a design decision:

LayerEnforced byWhat it removes
Mechanical — formatting, obvious bugsgofmt, go vet, goimportsThe argument. Brace style, indentation, import order, shadowed errors — there is one output, so no one debates it in review (Lesson 15).
Social — naming, API shape, what to leave outcode review, Effective Go, the proverbs, reading the stdlibThe drift. Judgement calls a formatter can't make, kept convergent by a shared, written-down culture rather than re-litigated per team.

The mechanical layer is the cheaper and more famous one: gofmt has exactly one output for any valid program, so the entire global "tabs vs spaces" debate costs Go zero engineer-minutes. But the mechanical layer is deliberately shallow — it never touches naming or design. Those are the social layer, and the proverbs are how the community keeps the social layer from fragmenting into a thousand local dialects, which is the precise failure mode (two teams, two languages) that motivated Go in the first place (Lesson 01).

2 · The proverbs that carry weight

Rob Pike's "Go Proverbs" are one-liners, and like all aphorisms most of their value is in the reasoning they compress. Here are the ones that change how you write code, each stated as the cost it removes — answering question 1 (what can I leave out?) and question 4 (behavior or concretion?) from Lesson 00.

"Clear is better than clever."
The single load-bearing proverb. Clever code optimizes the writer's moment; clear code optimizes every future read. When a clever one-liner and a plain three-liner do the same thing, the three-liner wins — because the reader's confusion is a real, recurring cost and the saved lines are paid once.
"The bigger the interface, the weaker the abstraction."
A one-method interface (io.Reader) can be satisfied by almost anything, so it composes everywhere (Lesson 13). A ten-method interface can be satisfied by almost nothing and welds callers to one shape. Small interfaces are more abstract, not less — a counterintuitive truth most languages get backwards.
"Accept interfaces, return structs."
Take the smallest behavior you need (so any caller's type fits), but hand back a concrete type (so callers see real fields and methods, and you can add more later without breaking them). Returning an interface hides capability and freezes your API at its narrowest point. (Detailed in §3.)
"A little copying is better than a little dependency."
Twenty lines copied is twenty lines you own and can read; a dependency is an import-graph edge, a version, a supply-chain surface, and someone else's release cadence forever (Lesson 12). The dependency's complexity is invisible until it breaks — copying makes the cost local and visible.
"Don't communicate by sharing memory; share memory by communicating."
Question 2 made a proverb (Lesson 09). Default to passing ownership down a channel — the data movement is then visible in the code — and reach for a mutex only when the shape is genuinely shared state (Lesson 11). It tells you which concurrency tool is the default and which is the exception.
"Errors are values."
Failure is ordinary data you handle, store, and program with — not a hidden control-flow channel (Lesson 07). The proverb licenses you to compute with errors: collect them, wrap them with %w, switch on them with errors.As, because they are just values like any other.
"Make the zero value useful."
Every type's zero value is reachable for free (Lesson 02); if it is also usable, callers skip a constructor and a whole class of "forgot to initialize" bugs vanishes. var b bytes.Buffer and var mu sync.Mutex are ready to use at zero. Design so the empty value is the valid value.

Notice none of these is about syntax. They are all about where you put complexity — in a smaller interface, a local copy, a visible error, a useful zero. That is the through-line: idiom is the bargain applied to design decisions too small for the language spec to legislate.

3 · Idiom as concrete API-design rules

The proverbs become real when you write a signature. Here are the decisions where idiom does the most work, with the idiom and the anti-pattern side by side.

Naming carries scope information. Go's convention is that name length tracks the distance between declaration and use: a loop index is i, a receiver is one or two letters, a package-level exported function gets a full descriptive name. And because export is just capitalization (Lesson 12), the name is the access modifier — Parse is public API, parse is internal. There is also no stutter: inside package http the type is Server, used as http.Server, never http.HTTPServer.

// anti-pattern: package-qualified name stutters, and the receiver over-names
func (theBuffer *Buffer) BufferReset() { ... }   // read as buf.BufferReset()

// idiom: the package already says "buffer"; the receiver is terse
func (b *Buffer) Reset() { ... }                 // read as buf.Reset()

Accept interfaces, return structs. This is the most consequential API rule in the language. Take the narrowest behavior you actually use as the parameter, so the widest set of caller types fits; return the concrete type, so callers keep full access and you can grow the type later.

// idiom: accepts anything that can be read; returns a concrete *os.File
func ReadConfig(r io.Reader) (*Config, error) { ... }

// the caller can pass a file, a network conn, a bytes.Buffer, a test fake —
// because io.Reader is one method (Lesson 13). If ReadConfig had demanded
// *os.File, none of those would fit.

// anti-pattern: returning an interface hides the concrete type's other
// methods and freezes the API at its narrowest surface
func OpenStore() Store { ... }   // caller can never reach a future *Store.Stats()

Define the interface where it is consumed, not where the type is defined. A C++/Java instinct is to ship an interface next to the implementation. Idiomatic Go puts the interface in the consuming package and keeps it as small as that consumer needs — because interface satisfaction is structural (Lesson 05), the implementer never imports it. This keeps the dependency arrow pointing the right way and the interface honestly minimal.

// package report (the consumer) declares exactly the behavior IT needs:
type Fetcher interface {
    Fetch(ctx context.Context, id string) ([]byte, error)
}
// package storage (the implementer) just has a method with that signature.
// It never imports report. The two are coupled only by a shape, not a name.

Errors: wrap with context, expose with sentinels or types. An error should accumulate context as it travels up (so the message says where), while staying programmatically inspectable (so a caller can branch on what). Wrap with %w; let callers test with errors.Is (sentinel) or errors.As (typed) — Lesson 07.

// idiom: add context with %w, preserving the chain for errors.Is/As
if err != nil {
    return fmt.Errorf("load config %q: %w", path, err)
}
// caller: errors.Is(err, os.ErrNotExist) still works through the wrap.

// anti-pattern: %v flattens the error to a string and breaks Is/As
return fmt.Errorf("load config: %v", err)   // chain is lost

Make the zero value do something. If var x T is immediately usable, the constructor becomes optional and a class of nil-deref and uninitialized-state bugs disappears. The stdlib lives this: sync.Mutex, bytes.Buffer, and time.Time all work at zero.

// idiom: zero value is ready; no New() required
type Set struct{ m map[string]struct{} }
func (s *Set) Add(k string) {
    if s.m == nil { s.m = map[string]struct{}{} }  // lazy init on first write
    s.m[k] = struct{}{}
}
var s Set; s.Add("x")   // works — no constructor, no panic

4 · The line between idiom and dogma

Here is the trap. A proverb is compressed reasoning; cargo-culting it without the reasoning is itself unearned complexity — the exact thing the bargain forbids. Idiom is a default, not a law, and the senior skill is knowing when the cost the proverb protects against isn't present.

Idiom earning its keep
  • A function reads from a source and you don't care what kind → take io.Reader. The abstraction is free and the caller set is huge.
  • You'd pull a 40-line dependency for one helper → copy the helper, cite the source in a comment. The graph edge costs more than the lines.
  • Two workers hand items off in sequence → a channel. Ownership movement is visible (Lesson 09).
Idiom turned into dogma
  • Defining a one-method interface for a type with exactly one implementation and no test fake — abstraction no caller benefits from. Just use the struct.
  • Copying 400 lines to avoid a battle-tested dependency — "a little copying" was about a little; now you own a fork.
  • Forcing a channel pipeline onto shared-counter state a sync.Mutex models in three lines (Lesson 11). Channels orchestrate; mutexes protect.

The discipline that keeps you on the right side of the line is not memorizing the proverbs — it is reading the standard library, which is the idiom made executable. When you're unsure how to shape an API, find the closest thing in io, net/http, or encoding/json and copy its taste (Lesson 13). The stdlib is the community's accumulated answer to "which form costs the reader least," and it is one go doc away.

Honest scope
Idiom has a real downside the series won't hide: a strong shared convention can ossify into "that's just how Go does it" reasoning that resists genuinely better designs, and the proverbs can be wielded as a cudgel in review by people who learned the slogan but not the cost behind it. The defense is the same in both directions: always be able to name the specific reader-cost a choice incurs or avoids. An idiom you can't justify in those terms is not an idiom you understand — it's a superstition you inherited.

Checkpoint exercise

Try it
Take a function you wrote in any prior lesson — ideally one that takes a parameter and returns something. Apply the three §3 rules in order. (1) Parameter: does it demand a concrete type where a one- or two-method interface would do? Narrow it, and list how many more caller types now fit. (2) Return: are you returning an interface where a struct would let callers see more? Widen it. (3) Zero value: is var x T usable, or does it panic until a constructor runs? Make the empty value valid if you can. For each change, write one sentence naming the reader-cost you removed — and for at least one rule, find a case where applying it would be dogma (no caller benefits) and say why you'd leave the code alone. That last step is the lesson.

Where this points next

You now have every piece — values and pointers, methods and interfaces, composition, errors, goroutines and channels and context, packages, the stdlib's taste, generics, tooling, and the idiom that chooses among them. Lesson 17, the capstone, spends all of it at once: we design one small concurrent service end to end — interfaces accepted and structs returned, errors wrapped up a call stack, goroutines coordinated by channels and stopped by a context, the whole thing packaged and tested — and walk the five questions over the finished design. Then it closes the series the way the C++ track does: an honest accounting of what the bargain cost us, so you leave able to defend Go's choices and name their price in the same breath.

Takeaway
"Idiomatic" is not style and not folklore: it is the Go bargain applied at the smallest scale, where the unit of complexity is a single reader's moment of hesitation — among forms the compiler treats as equal, the idiom is the one that costs the next person the least to read. Go enforces it in two deliberately separated layers: a mechanical one (gofmt, vet) that removes the argument about formatting by having exactly one output, and a social one (proverbs, review, the standard library) that keeps the unlegislatable judgement calls — naming, API shape, what to leave out — from fragmenting into local dialects. The proverbs that carry real weight are all about where you put complexity, not syntax: clear over clever; the bigger the interface the weaker the abstraction; accept interfaces, return structs; a little copying beats a little dependency; make the zero value useful. The transferable habit is to read each proverb as compressed reasoning about reader-cost and to apply it as a default rather than a law — because cargo-culting an idiom you can't justify is itself the unearned complexity the bargain exists to refuse.

Interview prompts