Book companion · MIT
Learn Go with Tests: summary and chapter guide
Quotations are used under the MIT license. Read it free.
What is Learn Go with Tests about?AI-assisted
Testing pain diagnoses hard-wired dependencies, not flaws in the TDD loop
How do you grow trustworthy Go software test-first? That one question drives the whole book. It opens with deliberately trivial exercises — Hello, Add, Repeat, Sum, Area, Wallet, Dictionary — each teaching one language feature while drilling the same loop: write a failing test, read the compiler, watch it fail for the right reason, write just enough code, refactor. It then climbs through goroutines, channels, select, context, reflection, generics and property-based testing; builds a full HTTP and CLI poker application chapter by chapter; and closes with acceptance testing, specification/driver separation, scaling TDD and a catalogue of anti-patterns. The through-line is that tests are a design tool, not a verification chore: pain in testing diagnoses hard-wired dependencies, so collaborators are injected behind small standard-library interfaces, failures are returned as inspectable values rather than strings, and abstractions are extracted only once concrete tested behaviour reveals them. Every lesson runs on small Go programs, so claims about large systems are argued rather than measured.
A fable for the whole bookAI-assisted
One story that carries the book’s central idea — and an honest note on where the story breaks.
Standard buckets eliminate the need for secret hatches and special access
Once there was a village whose well stood inside the miller's house, bricked into his hearth wall. Only the miller could draw water, and only by firelight. When the villagers asked for a taste of the water before baking day, the miller said: bake first, taste after. So they baked, and the bread came out bitter, and the miller swore the water was sweet, and nobody could prove otherwise without tearing down a wall. One spring a young cooper named Isa said: before I bake anything, I want a cup. She tried to fetch one and found she could not — not because the water was bad, but because the well had been built where no bucket could reach. Her neighbours called this a failure of her cup. Isa called it a failure of the wall. She did not cut a secret hatch for tasters only; she said the well should be where anyone with a bucket can stand, and the miller should be handed his water like everyone else. So the wall came down, the well went to the square, and the only thing a drawer of water now needed was a rope-and-bucket of the standard size — the cooper's bucket, the baker's bucket, the child's tin cup all fit the same rim. Isa drew one cup, tasted it, found it brackish, and the village knew before the ovens were lit. Then came Jorun, who hated walking to the square. He kept a painted bucket in his kitchen that he filled each morning from memory of how the well used to taste, and every evening he declared the water sweet. His bread was bitter too, but now his tasting said otherwise — a worse state than the miller's, who at least had admitted nobody could tell. The village settled on a rule: a bucket in the kitchen is allowed only if it is a real bucket holding real water, and only if the same tasting ritual is run against it and against the square's well, and both must agree. And once a year the whole village baked a loaf start to finish and ate it, because no cup of water, however honest, proves the bread.
What the story stands for
| In the story | In the book |
|---|---|
| Isa demanding a cup before baking, not after | Writing the test first and watching it fail for the right reason, because tests written afterwards go evergreen or assert irrelevant detail |
| Her failure to reach the water, and her reading it as a fault of the wall rather than of her cup | Pain in testing as design feedback about hard-wired dependencies, global state and too many collaborators — the fix belongs in production design |
| Refusing the secret taster's hatch and moving the well to the square instead | Rejecting test-only modes and layers invented for tests in favour of changing the real structure |
| The standard rim that the cooper's, baker's and child's vessels all fit | The small standard-library interface as a seam — io.Reader, io.Writer, a store — so one body of logic serves a CLI, an HTTP server and a browser |
| Handing the miller his water from outside like everyone else | Dependency injection: collaborators passed in rather than constructed inside or reached for as globals |
| Jorun's painted bucket filled from memory, declaring sweetness while the bread stays bitter | Mocks, stubs and spies as scattered unvalidated assumptions that let tests pass while the software is broken |
| The rule that a kitchen bucket must hold real water and pass the same ritual as the well | A stateful fake trusted because one reusable contract suite runs against both it and the real dependency |
| The yearly loaf baked and eaten whole | Acceptance tests driving the system from outside — confidence unit tests cannot give, costly enough to complement rather than replace them |
Where the fable breaks
The fable distorts by making the redesign a single irreversible act of masonry with an obvious villain. The book's version is iterative and cheap: the wall comes down a brick at a time, in thin end-to-end slices that get out of red fast — hard-coding a return value if need be — with the standard rim extracted only after concrete tested behaviour reveals the pattern, never carved up front. It also cannot hold the parts that live in types and tools rather than in moral geometry: errors as inspectable sentinel or typed values checked with errors.Is/As, generic and constrained types that make a bad bucket unrepresentable at compile time, benchmarks that turn speed claims into measurements, and the whole concurrency register where mutexes, channels and context do the serialising and sleeping is never synchronisation. Nor can it carry the book's own unresolved tensions — that the poker chapters build the HTTP server with exactly the stub and spy the fable condemns in Jorun, that no rule says when hard-coded slime must go, and that splitting specification from driver adds the very indirection the refactoring advice warns against. Finally, every lesson behind this story was demonstrated on deliberately trivial programs, so the village's confidence that one rim fits all vessels is argued rather than measured at scale.
Learn Go with Tests: the core arguments
10 theses the book defends, in the order the argument needs them.
- Because software in real use must keep changing and accrues complexity, unit tests are the precondition for continuous behaviour-preserving refactoring — so having no time to refactor is a signal it is overdue, not a reason to skip it.
“Those who choose not to write tests will typically be reliant on manual testing. For anything other than a small project this will be a tremendous time-sink and does not scale in the long run.”
Why unit tests and how to make them work for you › When refactoring code you must not be changing behaviourRead in context → - Tests must assert externally visible behaviour through a unit's exported API; coupling them to implementation detail or raw output strings produces misleading failures, brittle suites and obstructed redesign.
- A test must be written first and watched failing for the right reason, because tests written afterwards tend to be evergreen or to assert irrelevant detail, and example-based cases systematically miss domain truths that stated properties expose.
- Pain in testing is design feedback about hard-wired dependencies, global state or too many collaborators — TDD is conceptually simple, and what people blame on it is the difficulty of designing well that the loop merely exposes.
- Dependencies should be injected behind small interfaces the code already touches — io.Reader, io.Writer, a service or store — rather than hard-wired or wrapped in layers invented for tests, because the same seam that makes code testable also lets one body of domain logic back a CLI, an HTTP server and a browser.
- Code should be grown in thin end-to-end slices that get out of red as fast as possible — hard-coding if need be — with abstractions extracted only after concrete tested behaviour reveals the pattern, never invented up front.
- Mocks, stubs and spies are a liability because they scatter unvalidated assumptions that let tests pass while the software is broken, whereas stateful fakes checked by a contract run against the real dependency give integration-level confidence at unit-test speed.
- Acceptance tests driving the whole system from outside give confidence unit tests cannot, but cost enough that they must complement rather than replace them — and they stay reusable only when the specification is separated from the driver, so behaviour changes start in the specification and follow compiler errors downward.
- Correctness belongs in types and returned values rather than in strings and runtime luck: errors should be sentinel or typed values inspected with errors.Is/As, invalid inputs should be excluded by constrained or generic types, and the compiler should be used as a collaborator that moves failures to compile time.
- Concurrency and optimisation must be driven by evidence rather than hope: shared mutable state needs explicit serialisation and sleeping is never synchronisation, while speed claims are only legitimate once the code works, has been refactored, and a benchmark measures the gain.
Chapter by chapter
Every chapter is on this page: how the argument unfolds, its key claims, common misreadings and key concepts. Pick one from the list.
Meta: Why unit tests and how to make them work for you
How do you tell a unit's real boundary from an accidental one?
How Meta unfoldsAI-assisted
The chapter opens with a puzzle framed around the word "software" itself: if code is supposed to be malleable, why do so many projects collapse into legacy systems that must be rewritten? The author credits Go with several properties that help — a tiny keyword set, strong tooling for testing and benchmarking, a rich standard library, fast compilation, and the backward-compatibility promise (contrasted with a painful Scala upgrade) — but insists that no language prevents bad systems. That sets up the historical anchor: Manny Lehman's 1974 laws of software evolution. Two are singled out. The Law of Continuous Change says software must change or grow useless, which the author uses to criticize teams incentivized to ship by a date and hand off, and to argue that even excellent engineers can't foresee future needs. The Law of Increasing Complexity says complexity grows unless work is done to reduce it, warning against treating teams as "blind feature factories."
From there the chapter narrows to its chosen remedy. Among many practices that keep software soft — good separation of concerns, architecture, observability, deployability, tests, feedback loops — the author picks refactoring, noting that the word is widely misused, and quoting Martin Fowler to the effect that a system broken for days is not being refactored. The meaning is recovered through a schoolroom analogy: adding 1/2 and 1/4 by re-expressing 1/2 as 2/4 changes the form but not the value. Refactoring likewise reshapes code to fit your current understanding of the domain without changing behavior. A Go example makes it concrete: a Hello function stacked with per-language if branches becomes a lookup map plus a small greeting helper with a default. The author stresses that the particular rewrite doesn't matter — the one rule is that behavior is untouched, because changing behavior while refactoring means doing two hard things at once, which is where refactoring efforts fail. Since you can't verify equivalence in your head on a non-trivial system, and manual testing doesn't scale, unit tests supply the missing check: confidence, documentation of intended behavior, and fast feedback. A short TestHello illustrates the rhythm of small refactor, run tests, repeat — subsecond feedback as a safety net against Lehman's complexity.
The turn comes when the author confronts the counter-experience: many people find unit tests obstruct refactoring, which is the opposite of the promise. The diagnostic question is how often tests must change during a refactor. The illustration is a square built from two triangles, with tests asserting triangle properties; when a later developer rebuilds the square from rectangles, those tests fail misleadingly, even though the internal shapes were never the point. The lesson is that such tests falsely elevate implementation details. The prescription is to test behavior rather than implementation — the complaints usually trace to tests at the wrong abstraction level, over-spying on collaborators, excessive mocking, and chasing test coverage as a vanity metric. Pure black-box testing isn't the answer either: valuable for key journeys, but expensive, slow, and poor at pinpointing causes, so unhelpful for refactoring.
The resolution reframes testing as a design question. Aim for self-contained, decoupled units built around domain concepts — Lego bricks with coherent APIs, like an "account" package in a banking system, hiding whatever collection of types and functions makes them work. Test those public APIs and you are necessarily testing useful behavior, leaving the insides free to change. And yes, the author affirms, these count as unit tests; "unit" never meant a single function or class.
The closing section braids the three strands together: refactoring signals whether your tests sit at the right level (manual checks mean too few tests; wrongly failing tests mean wrong abstraction or no value) and manages complexity; unit tests provide the safety net and the documentation; well-designed units make both testing and refactoring easy. This leads into the case for TDD. Rather than the "bad old days" of months of requirements and design documents preceding failure — which, the author notes, still happens — Agile and TDD enforce iterative work with fast feedback: write a small test for a small behavior, watch it fail clearly, write the minimal code to pass, refactor, repeat. With practice this loop feels natural, and sitting outside "green" starts to feel like a warning sign. The wrap-up restates the thesis: change is inevitable but unpredictable, so don't over-engineer; keep software malleable through continuous refactoring, backed by a good test suite and meaningful, composable units, with TDD as the discipline that drives all three.
Key claims, with the lines that support them
A modern language with good tooling does not by itself protect a system from decaying into legacy.
The author lists what makes Go promising — a small keyword set, a strong standard library, fast compilation, first-class test and lint tooling, and a backward-compatibility promise he contrasts with weeks lost upgrading a Scala project — then concedes terrible systems can still be built with all of it, which is why he turns to Lehman's 1974 laws: software in real use must change or lose its value, and its complexity keeps rising unless effort goes into reducing it.
“Those who choose not to write tests will typically be reliant on manual testing. For anything other than a small project this will be a tremendous time-sink and does not scale in the long run.”
Why unit tests and how to make them work for you › When refactoring code you must not be changing behaviourRead in context →
Refactoring means improving the shape of code while leaving its behaviour exactly as it was; changing behaviour at the same time is a different, riskier job.
He borrows school arithmetic: rewriting 1/2 + 1/4 as 2/4 + 1/4 gives the same 3/4 but in a form easier to work with. His Go example does the same to code — a pile of per-language if statements becomes a greetings map plus a lookup helper — with the point being not the technique but that the output is unchanged. He cites Fowler's test too: if a system is broken for days, that work was not refactoring. Doing two things at once is where teams bite off more than they can chew.
“Those who choose not to write tests will typically be reliant on manual testing. For anything other than a small project this will be a tremendous time-sink and does not scale in the long run.”
Why unit tests and how to make them work for you › When refactoring code you must not be changing behaviourRead in context →
Because manual verification does not scale, unit tests are what make refactoring on a non-trivial system safe.
Teams without tests fall back on manual checking, a time sink that stops working past small projects. Tests instead supply three things: confidence to reshape code, documentation of intended behaviour for humans, and feedback far faster and more reliable than hand-checking. Concretely, a single test on the greeting function plus go test tells him in moments whether a change altered behaviour, which is what sustains the small-refactor / run-tests / repeat loop and keeps complexity in check.
“Those who choose not to write tests will typically be reliant on manual testing. For anything other than a small project this will be a tremendous time-sink and does not scale in the long run.”
Why unit tests and how to make them work for you › When refactoring code you must not be changing behaviourRead in context →
3 more claims
Tests should be aimed at behaviour visible through a component's public API rather than at how it is implemented inside.
Imagine a square built by joining two triangles, with tests asserting the triangles' angles sum to 180 degrees. Weeks later a developer decides two rectangles would be better and is met with failing tests that give no answer to the only question that matters — did real behaviour break? The triangle construction was never important, yet the tests had promoted it to the status of a requirement. The author traces this to misunderstanding what a unit is and to chasing coverage as a vanity metric.
“Favour testing behaviour rather than implementation detail”
Why unit tests and how to make them work for you › Favour testing behaviour rather than implementation detailRead in context →
Writing useful unit tests is really a design question about how the system is divided up.
He asks the reader to forget tests for a moment: you already want decoupled components centred on domain concepts, which he pictures as Lego bricks with coherent APIs that combine into larger systems while dozens of types and functions collaborate out of sight. A bank's "account" package is his example of an API that leaks no internals and is easy to integrate with. Get those boundaries right and behaviour-level tests come almost for free — which is his answer to the problem the square-and-triangles story exposed.
“Favour testing behaviour rather than implementation detail”
Why unit tests and how to make them work for you › Favour testing behaviour rather than implementation detailRead in context →
TDD is the working process that keeps this refactoring loop running, rather than trying to design the perfect extensible system in advance.
Taking Lehman's point that software must change, some people over-correct into elaborate upfront design — the era he describes of six months of requirements documents, six more of architecture, and a failed project years later, something he notes still happens. Since the future cannot be predicted, he follows Agile in working iteratively, and TDD enforces it: a small test for a small behaviour, a clear red failure, the minimum code to go green, then refactor, repeat. Practitioners come to expect that loop to be quick and feel uneasy whenever the system is not green, which signals a rabbit hole.
“Those who choose not to write tests will typically be reliant on manual testing. For anything other than a small project this will be a tremendous time-sink and does not scale in the long run.”
Why unit tests and how to make them work for you › When refactoring code you must not be changing behaviourRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Tests as a refactoring safety net
Reshaping code only counts as refactoring if behaviour stays identical, but on any sizeable system a person cannot hold all the behaviours in their head. Fast unit tests check that invariant automatically, so the author can take one small step, re-run them in under a second, and catch a behaviour change the moment it happens instead of hours later.
Mechanism 2
Why detail-bound tests block change
When tests assert how something is built rather than what it does, the build choice becomes frozen. A later developer who picks a different internal construction sees a wall of failures that say nothing about whether real behaviour broke, so they must excavate old tests first — and eventually stop attempting the change at all.
Mechanism 3
Unit boundaries decide test durability
If the system is carved into self-contained components built around domain concepts, each exposing a coherent public API that hides its internals, tests written against that API can only be asserting behaviour worth keeping. Everything beneath the API stays free to be rearranged, so refactoring rarely touches the tests.
Common misreadings
If the goal is to test behaviour rather than internals, the honest answer is to skip unit tests and rely on end-to-end or black-box tests.
System-level tests are genuinely valuable for verifying key user journeys, but the author rules them out as the refactoring safety net: they are expensive to write, slow to run, and poor at pointing to root causes, so their feedback loop is too slow to support small steps. Behaviour testing at the right altitude means testing through a unit's public API, which is both behaviour-focused and fast.
“Favour testing behaviour rather than implementation detail”
Why unit tests and how to make them work for you › Favour testing behaviour rather than implementation detailRead in context →
A unit test is by definition a test of one function, class or method, so testing a whole package through its API must be something else — an integration test.
The author answers this directly: these are unit tests, because the "unit" is the self-contained, domain-centred component with a coherent public API, not a single function or class. Many types and functions may cooperate beneath that API; treating the smallest code element as the unit is exactly what pushes tests down into implementation detail and makes them obstruct refactoring.
“Favour testing behaviour rather than implementation detail”
Why unit tests and how to make them work for you › Favour testing behaviour rather than implementation detailRead in context →
Key concepts in Meta
- Refactoring
- Reshaping code so it is easier to understand while leaving behaviour untouched, like factorising a fraction.
- Where it applies: If the system is broken for days, or behaviour shifts, it is not refactoring.
- Unit
- A self-contained, decoupled piece of the system built around a key domain concept, exposing a coherent public API.
- Where it applies: A unit is not a single class or function; many types may collaborate beneath its API.
- Lehman's laws of software evolution
- Software in real use must change or lose value, and its complexity grows unless work is spent reducing it.
- Where it applies: They justify iterative change, not months of upfront over-engineering.
0 more concepts
Reader’s notes on MetaAI reading companion
“Go has only 25 keywords”
Read in context →The implicit claim — fewer keywords means more durable code — never gets argued. Plenty of unmaintainable C exists, and C is small too. Constraint helps, but legacy comes from accumulated decisions, not vocabulary size.
“its complexity increases unless work is done to reduce it”
Read in context →The quiet word is "work". Complexity reduction is labor nobody books a ticket for, which is exactly why it loses to features. The law is less about software than about incentives.
“you can be pretty sure they are not refactoring”
Read in context →Fowler's line works as a diagnostic because it's falsifiable: if the build is red for two days, the name is wrong. Rare to get a definition that comes with a test attached.
8 more notes
“by changing 1/2 to 2/4 it fits into our "domain" easier”
Read in context →The analogy is charming but it flatters refactoring. Arithmetic has a decision procedure for equivalence; code doesn't. Tests are a sampling of behaviour, not a proof of it — the metaphor hides exactly where the risk lives.
“The nature of this refactor isn't actually important”
Read in context →Worth noticing that the example did change behaviour slightly in spirit: a map makes languages data, which invites runtime configuration the if-chain never allowed. Structure is a claim about the future.
“got := Hello(“Chris”, es)”
Read in context →Smart quotes and a bare es — this snippet wouldn't compile. In a book whose whole argument rests on tight, trustworthy feedback loops, an untested code sample is a small, funny indictment.
“our tests have falsely elevated the importance of our implementation details”
Read in context →This is the chapter's real thesis, and the triangles-to-rectangles parable earns it. Every failing test is a claim that something mattered; write enough careless claims and your suite becomes a monument to decisions you no longer hold.
“chasing vanity metrics (test coverage)”
Read in context →Fair, but coverage became a vanity metric because "test behaviour, not implementation" can't be measured and coverage can. The critique needs an alternative signal, and the chapter doesn't supply one.
“They were never about only being against a single class/function/whatever”
Read in context →A quietly radical move: redefining "unit" as a package-level API dissolves the mock-everything school by fiat. Persuasive, but it means two engineers can both say "we write unit tests" and mean incompatible things.
“If tests are wrongly failing then our tests are at the wrong abstraction level”
Read in context →This is unfalsifiable as stated — any painful test can be blamed on abstraction level rather than on the refactor being genuinely risky. A useful heuristic dressed as a verdict.
“don't try and over-engineer because it's too hard to predict the future”
Read in context →The honest tension of the whole chapter: designing good "units" is itself a prediction about which concepts will stay stable. TDD doesn't remove that bet, it just makes it cheaper to lose.
A reader’s takeAI reading companion
Unit redefined as package-level API is radical, not mere clarification
The chapter's architecture is stronger than any of its individual arguments: Lehman's laws establish that change is compulsory, refactoring is named as the response, tests are introduced as the only way to refactor without lying to yourself, and unit design is revealed as the thing that determines whether tests help or obstruct. That chain — and especially the final move, that writing good unit tests is a design problem rather than a testing problem — is the most valuable idea here, and the triangles/rectangles parable delivers it in a form you can actually carry into a code review. Where it's weakest is where it's most confident.
The factorisation analogy quietly borrows mathematics' certainty about equivalence, which tests cannot provide. The claim that a suite failing during refactor proves the tests were at the wrong abstraction level is circular enough to excuse any refactor. The attack on coverage is correct but leaves the reader without a replacement signal, which is precisely why coverage won in the first place. And redefining "unit" as a package-level API is a real contribution that the chapter presents as a mere clarification, when it's actually a contested position. The accidental comedy — a non-compiling test snippet in the chapter arguing for trustworthy feedback loops — is a decent reminder that the discipline is harder than the sermon.
Continue reading
Read it as a persuasive framing rather than a proof, and it's genuinely useful.
Read Meta with these cards beside each passage.
Open in FeedLitraGo fundamentals: Install Go, set up environment for productivity
How does go.sum hashing actually fail a build?
How Go fundamentals unfoldsAI-assisted
Opening: the environment as the first hurdle. The chapter begins with the barest possible gesture toward installation — a pointer to Go's official instructions — and then moves straight to what it evidently considers the more interesting problem: the Go environment itself. The first substantive section is about modules, introduced in Go 1.11 and the default build mode since 1.16, with the blunt conclusion that GOPATH is no longer recommended. Rather than simply asserting this, the text pauses to supply historical context: before modules, a single environment variable pointed at one directory (typically ~/go) where all Go code, dependencies, and compiled packages had to live, which meant you couldn't just make a folder anywhere and start writing code, because the compiler had no way to resolve imports outside it. That backstory does real work — it turns "use modules" from a rule into a solution to a remembered annoyance.
Development: three problems, three mechanisms, then the commands. The chapter then unpacks modules by mapping each stated goal onto a concrete mechanism. Dependency management gets go.mod, declaring required packages and versions (compared to Node's package.json); version selection gets Minimum Version Selection, the deterministic rule of picking the lowest version that satisfies every requirement, so outcomes are predictable; reproducible builds get go.sum and its cryptographic hashes, with the pointed detail that a build fails outright if a remote file has changed. A fourth point explains the GOPATH escape: a go.mod file anchors the project, and the toolchain resolves imports relative to that module, so a project can live anywhere. Only after this conceptual grounding does the text turn practical — mkdir, cd, go mod init, a sample go.mod — and it slips in a warning that reads as the chapter's small hidden trap: the go directive is not decorative, because the toolchain uses it to select language semantics. The example given is the Go 1.22 change to for loop variables, which the book's later concurrency chapter depends on; declare an older version and you will get confusing behaviour from correct-looking code.
Turn: from the toolchain to the person using it. After a brief note on linting (GolangCI-Lint as an upgrade over the default), the chapter pivots away from configuration toward craft, and this is its real argument. Because refactoring is a central emphasis of the book, your editor must make refactoring cheap: extract and inline variables, extract methods and functions, rename symbols across files, run go fmt on every save, and re-run tests immediately afterward — all by keystroke, not by effort. A companion list covers reading rather than changing code: view a function's signature and documentation, jump to its definition, find every usage of a symbol. The stated payoff is attentional rather than technical — mastering the tools keeps you thinking about the code instead of about the tools.
Close: a deliberately modest checkpoint. The wrap-up makes no grand claims. You should now have Go installed, an editor open, and some basic tooling configured; the chapter acknowledges that it has named only a few useful pieces of a very large third-party ecosystem and hands the reader off to awesome-go for the rest. The shape of the whole is thus a funnel that widens at the end: minimal installation talk, a deep dive into the one piece of environment that genuinely confuses newcomers, a turn toward editor fluency as preparation for the book's method, and then an open door rather than a conclusion.
Key claims, with the lines that support them
Modules, not the old GOPATH convention, are how Go projects should be organised today.
The author contrasts the two eras: before Go 1.11 every project, dependency and compiled package had to sit under one directory, so the compiler simply could not resolve imports for a folder created elsewhere. Modules remove that constraint by declaring dependencies and versions in a per-project go.mod, and since Go 1.16 this is the default build mode.
“Go 1.11 introduced Modules. This approach is the default build mode since Go 1.16, therefore the use of GOPATH is not recommended.”
Install Go, set up environment for productivity › Go ModulesRead in context →
Having established go.mod as the project's anchor, the author stresses that the Go version line inside it actually governs language behaviour rather than merely recording a number.
The toolchain reads that line to decide which language semantics apply, even when a newer Go is installed locally. The example given is the Go 1.22 change to how for-loop variables work: code in the later concurrency chapter that depends on the new behaviour only behaves as expected if go.mod declares 1.22 or above, which is why the author advises keeping the line reasonably current.
“The Go version declared here isn't just informational: the toolchain uses it to decide which language semantics apply, even if you have a newer version of Go installed.”
Install Go, set up environment for productivity › Go ModulesRead in context →
Turning from the toolchain to the person using it, the author treats fluency with your editor as part of the environment setup, not an optional extra.
Because the book leans heavily on refactoring, the author lists concrete operations you should be able to trigger with one key combination — extract or inline a variable, extract a function, rename a symbol across files, run go fmt on save, and re-run the tests immediately afterwards — plus navigation aids such as viewing a function's signature and definition and finding a symbol's usages. The stated payoff is staying focused on the code and switching context less.
“You should be familiar enough with your editor to perform the following with a simple key combination:”
Install Go, set up environment for productivity › Refactoring and your toolingRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
How go.mod frees code from one folder
Once Go spots a go.mod file in a directory, that file becomes the project's anchor and every import is resolved relative to that module. Because nothing has to be looked up through a single global directory any more, a project folder placed anywhere on the machine still compiles.
Mechanism 2
Why version conflicts resolve predictably
When several dependencies ask for different versions of the same package, Go does not guess or take the newest; it applies Minimum Version Selection and takes the lowest version that still satisfies every requirement. Because the rule is fixed, the same inputs always yield the same chosen versions.
Mechanism 3
Why everyone gets an identical build
Alongside go.mod, Go writes a go.sum holding a cryptographic hash of each dependency. Every later build re-downloads those files and checks them against the recorded hashes, so if anything on the remote server has changed, the build stops instead of silently using different code.
Common misreadings
The go 1.24 line in go.mod is just a note recording which Go version the author happened to use, so it can be left at whatever value was generated.
That line selects which language semantics the compiler applies, independently of the Go version installed on your machine. The author's own example is the Go 1.22 for-loop variable change: without a go.mod declaring 1.22 or later, code relying on it will not behave as intended.
“The Go version declared here isn't just informational: the toolchain uses it to decide which language semantics apply, even if you have a newer version of Go installed.”
Install Go, set up environment for productivity › Go ModulesRead in context →
Because modules let code live anywhere, GOPATH is gone and it no longer matters where the project folder sits.
GOPATH still exists; what changed is that relying on it is no longer recommended. The author's instruction is explicit: pick a directory outside GOPATH as the project root and run go mod init there.
“Go 1.11 introduced Modules. This approach is the default build mode since Go 1.16, therefore the use of GOPATH is not recommended.”
Install Go, set up environment for productivity › Go ModulesRead in context →
Key concepts in Go fundamentals
- Go module
- A project rooted at a go.mod file that declares its path, Go version and dependencies, so code can live anywhere.
- Where it applies: Default build mode since Go 1.16; the project root must sit outside GOPATH.
- Minimum Version Selection
- When dependencies disagree on a version, Go deterministically picks the lowest version that satisfies all of them.
- Where it applies: Applies to version selection among module requirements, not to hash verification.
- Refactoring
- Reshaping code safely; this chapter frames it as a tooling skill, not just a coding virtue.
- Where it applies: Depends on fast tests to confirm nothing broke.
0 more concepts
Reader’s notes on Go fundamentalsAI reading companion
“therefore the use of GOPATH is not recommended”
Read in context →Blandly stated, but this sentence buried a whole era of Go pain. The chapter only lets you feel that in the next paragraph.
“you could not simply create a folder anywhere on your computer and start writing Go code”
Read in context →This is the real indictment of GOPATH — a language dictating where your files live. Strange how long it was tolerated as normal.
“Go always picks the minimum version that satisfies everyone”
Read in context →Counterintuitive and deliberately so — most ecosystems grab the newest and call it progress. Minimum means boring, and boring means reproducible.
5 more notes
“If anything changes on the remote server, the build fails immediately”
Read in context →Failing loudly is the feature. A silently mutated dependency is the worse outcome, though anyone who has fought a go.sum mismatch may not feel grateful in the moment.
“The Go version declared here isn't just informational”
Read in context →The sharpest practical warning in the chapter. A number you'd skim past silently changes loop semantics — exactly the kind of bug you'd never think to look for.
“brew install golangci-lint”
Read in context →One macOS command, offered as if universal. A setup chapter that quietly assumes your machine.
“You should never be unsure how to call a function in Go”
Read in context →A bold claim about certainty, and really a claim about tooling, not the language. The confidence is outsourced to your IDE.
“Mastering your tools will help you concentrate on the code”
Read in context →The thesis of the chapter, and the reason it isn't filler: keystroke fluency is what makes refactoring cheap enough to actually do.
A reader’s takeAI reading companion
Setup isn't really setup: it's testing whether refactoring is frictionless
An install chapter that's really an argument chapter. Two moves stand out. First, the modules section refuses to just say 'run go mod init' — it explains what GOPATH took away, and in doing so makes the module file feel like a liberation rather than boilerplate. The trio of problems (dependency management, version selection, reproducible builds) gets answered point by point, and Minimum Version Selection is the quiet gem: a choice against novelty, for predictability. Second, the tooling list is doing TDD's groundwork. The book's whole method depends on refactoring being frictionless, so the author converts an abstract value ('refactoring matters') into a concrete demand: can you extract a function with one key combination? That reframing is the chapter's real contribution. The weak spots are the edges — a single brew command standing in for all platforms, and golangci-lint recommended without saying what the default linter actually lacks. And the go.mod version note is the most load-bearing paragraph here, easily skimmed, yet it's the one that will save a reader from a genuinely baffling afternoon in the concurrency chapter.
Read Go fundamentals with these cards beside each passage.
Open in FeedLitraGo fundamentals: Hello, World
How do subtests change the way requirements are expressed?
How Go fundamentals unfoldsAI-assisted
The chapter opens with the most conventional starting point possible and uses it as bait. You create a folder, write a six-line hello.go, run it with go run, and then get a quick tour of what you just typed — the main package, the func keyword, the imported fmt. Almost immediately the chapter pivots from "does it run" to "can you test it," and that pivot carries the real argument: printing to stdout is a side effect, while the string being printed is the domain. So the code is split — Hello() returns a string, main prints it — and a hello_test.go file appears alongside it. A practical detour follows about Go modules, since on Go 1.16 and later go test fails outright until you run go mod init example.com/hello; the text explains what the module file is for, why the name should contain a dot, and that the module name is unrelated to package main. Then the test passes, and the author invites you to break it on purpose — the first hint that seeing failure is part of the method, not an accident.
From there the chapter settles into teaching by accumulation. It states the mechanical rules for tests (xxx_test.go, a Test prefix, the single t *testing.T argument, importing testing), unpacks the small syntax encountered along the way — if, := declarations, t.Errorf with its %q verb — and then detours into Go's documentation tooling, go doc offline and pkgsite for the browser version. The turn comes with "Hello, YOU": having written one test after the fact to demonstrate the shape of a test, the author announces that from now on tests come first. Each new requirement then runs the same loop. Greeting a specific person starts as a compilation error — too many arguments — which the text treats as the compiler telling you your next move; you widen the signature, watch the test fail on content, concatenate, pass, then refactor the literal into a constant. A note on source control sits in the middle of this: commit the working version, but don't push, because refactoring comes next.
The loop then repeats twice more with rising stakes. Defaulting an empty name to "World" introduces subtests via t.Run, and once those pass the author refactors the tests — pulling the assertion into a helper that takes testing.TB rather than *testing.T so benchmarks can use it, and calling t.Helper() so failures report the caller's line number. Adding a language parameter breaks every existing call site again, and the fix follows the same rhythm: compile, fail, pass with an if, then refactor away the magic strings. French is handed to the reader as an exercise, and the second if it produces motivates switch — with an aside distinguishing := from =. A final refactor extracts greetingPrefix, which introduces the named return value prefix string, zero values, bare return, default in a switch, the capital-versus-lowercase convention for exported names, and grouped const blocks.
The chapter closes by naming the discipline it has been enacting all along — write a failing test, satisfy the compiler, see the failure and check that its message is legible, write just enough code to pass, refactor — and defending each step, especially the insistence on witnessing failure, since unreadable test output is its own kind of debt. The wrap-up inventories the Go syntax covered and the TDD rationale, then concedes the obvious: Hello() growing into Hello("name", "French") is trivial next to real software. The claim is that the increments, not the example, are the lesson.
Key claims, with the lines that support them
Code only becomes testable once the part that decides what to say is separated from the part that talks to the outside world.
The author starts from a program whose whole body is a print call and shows there is no handle for a test. Rewriting it so a function returns the greeting and main does the printing gives the test something to capture and compare, and every later requirement — names, empty input, other languages — is then driven entirely through that returned value.
“The fmt.Println is a side effect \(printing to stdout\), and the string we send in is our domain.”
Hello, World › How to testRead in context →
Watching a test fail before writing the code is a real check, not a ritual.
The author argues from the developer's own experience: a codebase whose failing tests do not explain the problem is painful to work in, so the red run is the moment to judge whether the message is informative. The same run also proves the test is wired to the new requirement rather than passing by accident.
“Seeing the test fail is an important check because it also lets you see what the error message looks like.”
Hello, World › DisciplineRead in context →
Following on from that, tests should be written before the code rather than reconstructed afterwards.
The author writes the first test after the fact only as a teaching example and then switches order, warning that a test written to match code already working can keep passing even when the behaviour is wrong — the test has been shaped by the implementation instead of by the requirement.
“Seeing the test fail is an important check because it also lets you see what the error message looks like.”
Hello, World › DisciplineRead in context →
2 more claims
The compiler is a usable guide through the small steps, not an obstacle.
Each new requirement first produces a compile error — too many arguments, then not enough arguments — and the author points out that the message states exactly what the next edit is: add a parameter, or pass a value at the call sites. Listening to it means the design of each step does not have to be worked out in advance.
“Seeing the test fail is an important check because it also lets you see what the error message looks like.”
Hello, World › DisciplineRead in context →
Refactoring applies to the tests themselves, not only to the shipped code.
After the empty-string requirement passes, the duplicated got/want comparison across subtests is pulled into one assertion helper that takes the broader testing.TB type so it can serve benchmarks too, and marks itself as a helper so failures still point at the right scenario. The production code gets the same treatment: repeated string literals become named constants, the stack of language checks becomes a switch, and the prefix lookup is finally extracted into a lowercase, and therefore private, function.
“Seeing the test fail is an important check because it also lets you see what the error message looks like.”
Hello, World › DisciplineRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Pulling output out of logic
Printing to the screen is an effect the test cannot inspect, so as long as the greeting and the printing live in one function there is nothing to assert on. The author splits them: a function returns the greeting string, and main merely prints whatever it returns. The returned value can then be compared against an expected value, which is what makes the whole test-first loop possible.
Mechanism 2
Failing once on purpose
Running a test before the code exists forces two things into the open: whether the test actually exercises the new requirement, and what its failure text looks like. If the test passes before you write anything, it was testing nothing; if it fails with a vague message, you learn that now rather than months later when a build breaks. That is why the author treats the red run as a step, not a formality.
Mechanism 3
Why helpers mark themselves
When a shared assertion function reports a failure, the test framework by default points at the line inside that helper, which is the same line for every failing case and tells you nothing. Marking the function as a helper makes the framework report the calling line instead, so the output names the specific scenario that broke. The author suggests commenting the marker out and failing a test to see the difference.
Common misreadings
The name you give to go mod init matters to the program — it should probably be "main", to match the package declared at the top of every file so far.
The module name is just a label for the project as a whole and is unrelated to the package name; running go run, go test or go build from that folder works with whatever name you pick. The author's one practical constraint is to put a dot in it, as in example.com/hello, so that tools used later behave.
“It's worth being clear that SOMENAME, the module's name, is unrelated to package main (which every .go file in this book so far declares at the top).”
Hello, World › Go modules?Read in context →
Since both assign a value, := and = are two spellings of the same thing and you can pick either.
They do different jobs: := declares a new variable and gives it a first value, while = changes a variable that already exists. In the refactored greeting function, prefix gets := once at its declaration and plain = inside the switch, and name — already a parameter — gets plain =. Using := on something already declared in the same scope, or = on something never declared, will not compile.
“Using := on a variable that already exists in the same scope, or = on one that doesn't exist yet, is a compile error.”
Hello, World › switchRead in context →
Key concepts in Go fundamentals
- TDD cycle
- Write a test, make it compile, watch it fail with a clear message, write just enough code to pass, then refactor.
- Where it applies: Needs fast tests and small steps; writing tests after the code risks tests that pass even when behavior is wrong.
- Go module
- A project-level identifier file created by go mod init that tells Go tools about your code and dependencies.
- Where it applies: Required from Go 1.16 onward before go test or go build; its name is unrelated to package main.
- t.Helper()
- A call that marks a function as a test helper so failures report the caller's line number instead of the helper's.
- Where it applies: Only affects reported line numbers in test output; helpers should take testing.TB to serve tests and benchmarks.
- var vs :=
:=declares a brand-new variable with an initial value; plain=reassigns one that already exists.- Where it applies: Mixing them up is a compile error: := on an existing variable in the same scope, or = on an undeclared one.
0 more concepts
Reader’s notes on Go fundamentalsAI reading companion
“It is good to separate your "domain" code from the outside world \(side-effects”
Read in context →The whole architecture of the book is smuggled in here as a casual aside. Testability isn't a property you add later — it's the reason the seam exists at all.
“go: cannot find main module; see 'go help modules'”
Read in context →Rare and admirable: a tutorial that shows you the error you will actually hit instead of pretending the happy path is the only path.
“it is important to listen to the compiler”
Read in context →The compiler is being recast as a pair programmer rather than a gatekeeper. That reframe is the real lesson of the chapter, not the string concatenation.
7 more notes
“there is the risk that your test may continue to pass even if the code doesn't work as intended”
Read in context →The sharpest sentence here. A test written after the fact tests your memory of what you built, not your requirement.
“I wouldn't push to main though, because I plan to refactor next”
Read in context →A glimpse of actual working habit, not doctrine — commit as a safety rope, not as a publication. The later "amend the previous commit" quietly admits the first commit was scaffolding.
“comment it out, make a test fail and observe the test output”
Read in context →Teaching by deliberate breakage. Far better than asserting what t.Helper() does — it makes the reader generate the evidence themselves.
“it defaults to printing "Hello, World", rather than "Hello,”
Read in context →Note the capital W creeping in — the constant said "world" at the start of the chapter. A tiny inconsistency, and exactly the kind a test would have caught.
“Fix them by passing through empty strings”
Read in context →The moment TDD's cost shows: adding a parameter breaks every existing call site. The book moves past it in one line, but this is where real codebases bleed.
“We don't want the internals of our algorithm exposed to the world”
Read in context →"Algorithm" for a three-case switch is generous. Still, the point lands: visibility in Go is a single letter, so encapsulation is a typing decision you make constantly without noticing.
“You can return whatever it's set to by just calling return”
Read in context →Introduced approvingly, but naked returns are widely regretted in Go style guides. The function is small enough to survive it; most aren't.
A reader’s takeAI reading companion
The hardest lesson for beginners: treating failure as information
A chapter that is only nominally about Hello, World — it's really a drill in a feedback loop, with Go syntax as the excuse. Its best move is refusing to be clean: it shows the module error, the arity error, the wrong-string failure, and tells you to comment out t.Helper() to watch the line numbers go bad. Failure is treated as information rather than embarrassment, which is the single hardest thing to teach a beginner. The structural choice is also smart: every new language feature arrives because a requirement demanded it — the constant to kill a magic string, the switch to kill stacked ifs, the extracted function to kill a growing body.
Nothing is introduced as vocabulary to memorize. Where I'd push back: the frictions are smoothed too fast. 'Fix them by passing through empty strings' waves past the real pain of changing a signature, and the final refactor sells a named return and naked return as clarity when the Go community largely treats them as a smell outside tiny functions. There's also a small self-inflicted wound — 'world' becomes 'World' mid-chapter — which is funny in a book whose thesis is that tests catch exactly this. None of that undercuts the core argument. The chapter's quiet claim is that testability is a design property, not a testing activity: the very first move is splitting fmt.
Continue reading
Println from the string, and everything after is downstream of that seam. That's the idea worth carrying forward.
Read Go fundamentals with these cards beside each passage.
Open in FeedLitraGo fundamentals: Integers
How does Property Based Testing actually close the hardcoding loophole?
How Go fundamentals unfoldsAI-assisted
Opening — setting up a clean workspace. The chapter begins by waving off any suspense: integers in Go behave just as you'd expect, so the real subject is practice rather than surprise. Before any code, it pauses on housekeeping — Go allows only one package per directory, so the reader is shown a sample project tree with helloworld and integers as sibling folders under a single go.mod. The first file written is the test, adder_test.go, declaring package integers rather than main, which signals the shift from a runnable program to a reusable library. The test calls Add(2, 2), expects 4, and reports failure with %d instead of the %q used for strings — a small, deliberate note that format verbs track the type being printed.
Development — the TDD cycle, run again at a faster tempo. The chapter then walks the familiar loop. Running go test yields a compilation error (undefined: Add), which counts as the failing step; the reader writes only enough code to compile — a stub returning 0 — so the test can fail for the right reason, printing the expected-versus-got message. Two asides ride along here: the shorthand (x, y int) for repeated parameter types, and a note that named return values, introduced in the previous chapter, are deliberately not used, because the meaning of Add's result is already obvious from context. Then comes the chapter's wry turn: a pedantic programmer could satisfy the test by returning a hardcoded 4. The author concedes the point ("TDD is a sham right?"), declines to play cat-and-mouse with more test cases, and promises property-based testing later as the real answer — before simply writing return x + y.
Turn — from correctness to communication. With the test green, refactoring finds little to improve in the three-line function, so attention moves outward to documentation. A doc comment is added above Add, and the chapter argues the broader principle: a reader should be able to understand your code from its signature and docs alone. This opens into the chapter's substantive new material, testable examples. ExampleAdd lives in the _test.go file, prints a result, and carries the magic comment // Output: 6. The payoff is explained in terms of a familiar failure mode — examples in README files drift out of date because nothing checks them — whereas example functions are compiled with the test suite, and the Output comment makes them actually execute, so stale documentation breaks the build. Verbose test output shows ExampleAdd running alongside TestAdder; removing the comment makes it compile but not run, which is noted as useful for code that can't run as a unit test, such as anything touching the network.
Closing — seeing the docs for real, then a short recap. The chapter ends practically, by making the documentation visible: install pkgsite, run pkgsite -open ., and browse to localhost:8080, where the reader can drill from the package list into Integers, then func Add, then expand the example they just wrote. It notes that publishing to a public URL surfaces the same documentation on pkg.go.dev. The wrap-up lists four takeaways: more TDD practice, integers and addition, better documentation for your users, and examples that are verified by your tests — a closing that quietly reframes the chapter as being less about arithmetic than about documentation you can trust.
Key claims, with the lines that support them
Before writing the real implementation, write just enough code to compile and watch the test fail for the reason you expect.
The author stubs Add to return 0 purely to satisfy the compiler, runs the test, and gets "expected '4' but got '0'" — a message that confirms the test is exercising the intended function and comparing the intended values before any real logic exists.
“Write enough code to satisfy the compiler and that's all - remember we want to check that our tests fail for the correct reason.”
Integers › Write the minimal amount of code for the test to run and check the failing test outputRead in context →
Passing a test is not the same as being correct, so the minimal-code step is a discipline for checking the test, not a proof about the code.
Following strict TDD, a pedantic programmer can make the suite green by having Add simply return 4. The author concedes this looks like it discredits TDD, notes that piling on more hand-chosen numbers is a game of cat and mouse, and defers the real answer to property-based testing later in the book.
“Write enough code to satisfy the compiler and that's all - remember we want to check that our tests fail for the correct reason.”
Integers › Write the minimal amount of code for the test to run and check the failing test outputRead in context →
Documentation should be executable so it cannot fall out of step with the code.
The author's starting point is that examples kept outside the codebase — in a readme, say — go stale because nothing checks them. The fix is an ExampleAdd function that go test -v runs and reports as a passing test alongside TestAdder; if the code later changes so the example no longer holds, the build fails. The condition is that execution only happens for examples carrying an output comment.
“Often code examples that can be found outside the codebase, such as a readme file, become out of date and incorrect compared to the actual code because they don't get checked.”
Integers › Testable ExamplesRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Why a deliberately wrong stub helps
By writing only enough code to compile — Add returning 0 — you force the test to actually run and print a real comparison. Seeing "expected 4 but got 0" proves the test is wired to the right function and measures the right thing, so a later pass can be trusted rather than assumed.
Mechanism 2
Examples the compiler keeps honest
Example functions live in the package's _test.go files, so they are compiled every time tests run, and an // Output: comment makes Go execute them and compare the printed result. Because a stale example then fails to compile or mismatches, documentation cannot silently drift away from the code it describes.
Mechanism 3
One test pins only one input
A test that checks Add(2,2) constrains behavior at exactly that point, so returning the constant 4 satisfies it completely. Chasing this with more hand-picked numbers is an endless cat-and-mouse game, which is why the author points ahead to property-based testing as the systematic fix.
Common misreadings
Any function named ExampleSomething will automatically be run and verified by Go.
Compilation and execution are two different guarantees. Every example is compiled, but it is only executed and checked when it carries an // Output: comment — remove that line and the example quietly stops running. Examples without the comment are still useful for code that can't run as a unit test, such as code touching the network, since they at least guarantee it compiles.
“If ever your code changes so that the example is no longer valid, your build will fail.”
Integers › Testable ExamplesRead in context →
Since named return values show up in documentation and editors, you should name returns everywhere to make code more usable.
The author treats naming as a clarity tool, to be used when the meaning of the result isn't obvious from context. Add is left with a plain int return precisely because a function called Add that takes two integers makes its result self-evident.
“It should generally be used when the meaning of the result isn't clear from context, in our case it's pretty much clear that Add function will add the parameters.”
Integers › Write the minimal amount of code for the test to run and check the failing test outputRead in context →
Key concepts in Go fundamentals
- Testable Examples
- Functions named ExampleXxx in _test.go files that act as documentation and are compiled, and executed when they carry an Output comment.
- Where it applies: Examples lacking an output comment are only compiled, not executed.
- Parameter type shorthand
- When consecutive parameters share a type you may list the type once, as in (x, y int).
- Where it applies: Only applies to adjacent parameters of the same type.
- Named return values
- Giving the result a name so it shows up in docs and editors; useful only when the result's meaning is unclear.
- Where it applies: Skipped in Add because the meaning is obvious from context.
0 more concepts
Reader’s notes on Go fundamentalsAI reading companion
“Go source files can only have one package per directory”
Read in context →A language rule smuggled in as a note. It's actually the chapter's real lesson — directory layout is package layout, with no way to opt out.
“we're using %d as our format strings rather than %q”
Read in context →Nice continuity — the previous chapter's quoting habit gets explicitly unlearned. Teaching by contrast with your own earlier code is underrated.
“Ah hah! Foiled again, TDD is a sham right?”
Read in context →The author voices the skeptic instead of suppressing them. And then — honestly — doesn't fully answer, just defers to a later chapter. I respect the IOU more than a hand-wave would deserve.
5 more notes
“that feels like a game of cat and mouse”
Read in context →The quiet admission: example-based tests can't prove generality, only sample it. Property-based testing is the right escape, but note the real argument is "don't be adversarial to yourself," which is social, not technical.
“It should generally be used when the meaning of the result isn't clear from context”
Read in context →Good restraint. A lesser tutorial would apply every new toy immediately; here the author declines to use the thing just taught, and says why.
“become out of date and incorrect compared to the actual code because they don't get checked”
Read in context →This is the sharpest idea in the chapter. Documentation rots because nothing fails when it lies. Examples work by giving docs a failure mode.
“temporarily remove the comment // Output: 6, then run go test”
Read in context →A comment that changes execution semantics is genuinely strange for Go. Asking the reader to delete it and watch the behavior change is the cheapest possible way to make that stick.
“useful for demonstrating code that cannot run as unit tests”
Read in context →So "compiles" is itself a tier of correctness worth buying. A tidy ladder: compiles < runs < asserts output.
A reader’s takeAI reading companion
Testable examples give documentation the failure pressure code gets
A deliberately small chapter: Add(x, y int) is a pretext. Two things are actually being taught. First, that the TDD loop survives contact with a trivial problem — and the author is candid that at this scale the discipline looks absurd, letting the pedantic return 4 stand as a real objection rather than pretending the method is airtight. He doesn't resolve it; he hands out a promissory note for property-based testing, which is honest but leaves the chapter's central doubt open. Second, and more lasting: documentation should be subject to the same failure pressure as code. Testable Examples are the best idea here, because they identify why README snippets rot — nothing breaks when they lie — and fix it structurally rather than by exhortation.
The // Output: comment, a comment that governs execution, is the one genuinely odd corner of Go on display, and the instruction to delete it and watch is the chapter's best teaching move. The pkgsite walkthrough is the weakest part — the most date-stamped, tool-version-fragile material in an otherwise durable chapter. Overall tone: a writer who trusts the reader enough to voice their skepticism aloud and to decline using a feature he just introduced.
Read Go fundamentals with these cards beside each passage.
Open in FeedLitraGo fundamentals: Iteration
How would Repeat change if the caller could choose the repeat count?
How Go fundamentals unfoldsAI-assisted
Opening — a constraint framed as a gift. The chapter opens by announcing that Go has exactly one looping keyword, for: no while, no do, no until — and the author insists "which is a good thing." It then hands the reader straight back to the TDD cycle already practiced in earlier chapters, with the nudge that "there's nothing new so far, so try and write it yourself." The worked example is deliberately trivial: Repeat("a") should return "aaaaa". The test comes first and fails to compile (undefined: Repeat); the fix is the smallest possible stub returning "", chosen not to pass but to let the reader confirm the test itself is well written. The author pauses here for a small morale note — you already know enough Go to test basic problems — before showing the honest failure message comparing "aaaaa" to the empty string.
Development — the loop, then polish. The passing implementation is a plain C-style three-clause loop accumulating repeated = repeated + character. The prose uses it to teach two incidental points of Go syntax: no parentheses around the loop header, braces always mandatory, and the distinction between var repeated string and the := shorthand used so far (:= being declaration plus initialization in one step, so the explicit var form is needed when you only want to declare). The refactor step then does what refactor steps in this book always do — change nothing observable — by extracting the magic 5 into const repeatCount and swapping concatenation for the += "Add AND assignment" operator.
The turn — from correctness to cost. The chapter's real pivot is benchmarking, which it presents as a first-class language feature that looks almost identical to a test: a BenchmarkRepeat(b *testing.B) function whose body loops on b.Loop(), run via go test -bench=.. The author explains that the framework picks the iteration count for you, that only the loop body is timed (setup and cleanup are excluded), and reads off a sample result of ~136 ns/op over ten million runs. That measurement becomes the lever for a performance lesson: Go strings are immutable, so every concatenation copies memory. Rewriting Repeat with strings.Builder and WriteString (remembering the final .String() call) and re-running with -benchmem drops the figure to ~25.7 ns/op at 8 B/op and 1 alloc/op — evidence rather than assertion.
Close — exercises and a short ledger. The chapter ends by handing the work back: generalize the test so callers choose the repeat count, write an ExampleRepeat as documentation, and go browse the strings package writing tests against whatever looks useful, on the argument that time spent learning the standard library compounds. The wrap-up is three lines — more TDD practice, for, and benchmarks — which is a fair accounting of a chapter that used a five-character string to smuggle in Go's measurement culture.
Key claims, with the lines that support them
Go offering a single loop keyword is an advantage rather than a shortcoming.
The author shows the ordinary three-part loop handles the whole task, and notes the syntax is familiar from C-like languages apart from dropping the parentheses and always requiring braces; the same loop then survives a refactor that pulls the count into a constant and switches to the add-and-assign operator, so nothing is missing without while, do or until.
“In Go there are no while, do, until keywords, you can only use for. Which is a good thing!”
IterationRead in context →
Benchmarks are a built-in part of Go testing, so performance questions can be settled by measurement.
A benchmark function written almost exactly like a test, run with the bench flag, reports an average time per operation; adding the memory flag also reports bytes allocated and allocation counts per operation. Having that in hand, the author compares two implementations of the same function instead of reasoning about which should be faster.
“Strings in Go are immutable, meaning every concatenation, such as in our Repeat function, involves copying memory to accommodate the new string.”
Iteration › BenchmarkingRead in context →
Naive concatenation in a loop costs real performance, and the standard library's string builder fixes it.
The concatenating version averaged about 136 nanoseconds per call, while the builder version came in at 25.70 nanoseconds with 8 bytes and a single allocation per operation — a direct before-and-after on the same benchmark. The author flags that these figures come from their own machine, so the absolute numbers are machine-specific even though the gap is the point.
“Strings in Go are immutable, meaning every concatenation, such as in our Repeat function, involves copying memory to accommodate the new string.”
Iteration › BenchmarkingRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Why repeated joining of strings is slow
Because a Go string can never be modified in place, each join has to allocate a fresh string and copy the old contents into it, so a loop that appends repeatedly keeps paying for copies. A builder writes the pieces into one growing buffer instead and hands back the finished string at the end, which removes most of that copying.
Mechanism 2
How the benchmark loop measures code
The benchmark receives a *testing.B and loops while b.Loop() keeps returning true, with the framework deciding how many rounds give a stable reading. Only what sits inside that loop is clocked, so preparation before it and teardown after it never inflate the per-operation figure.
Mechanism 3
Stub first so the failure can be read
The first version of the function returns an empty string and nothing else, purely so the package compiles. Running it then produces a real failure message comparing the wanted and actual values, which proves the test itself is written correctly before any logic is attempted.
Common misreadings
Declaring with var and declaring with := are two different mechanisms, and the short form is always the one to reach for.
The short form is just a compressed way of doing a declaration and an initialization together; var does the declaration on its own. Here the accumulator needs to start as an empty string before the loop runs, so the longer form is the natural fit — and var also declares other things, such as functions.
“However, := is simply short hand for both steps. Here we are declaring a string variable only.”
Iteration › Write enough code to make it passRead in context →
You should pick or tune how many iterations the benchmark runs, since that number drives the result.
The framework picks a count that yields a reasonable measurement, and the total only becomes available afterwards; what matters is the reported time per operation. Your job is to put the code under measurement inside the loop and leave setup and cleanup outside it.
“The testing.B gives you access to the loop function. Loop() returns true as long as the benchmark should continue running.”
Iteration › BenchmarkingRead in context →
Key concepts in Go fundamentals
- for (Go's only loop)
- The single repetition keyword in Go; no parentheses around its three parts, braces always required.
- Where it applies: This chapter shows only the three-component C-style form and b.Loop(); other variants are linked, not explained.
- Benchmark
- A test-like function taking *testing.B that loops with b.Loop() while the framework times only the loop body.
- Where it applies: Run with go test -bench=.; by default benchmarks run sequentially, and the iteration count is chosen by the framework.
- strings.Builder
- A standard-library type whose WriteString avoids repeatedly copying memory when building a string.
- Where it applies: You must call the String method to get the final result.
- var vs :=
:=is shorthand for declaring and initializing at once;vardeclares a variable (or function) without initializing it.- Where it applies: Used here only to declare an empty accumulator string before the loop.
0 more concepts
Reader’s notes on Go fundamentalsAI reading companion
“In Go there are no while, do, until keywords, you can only use for. Which is a good thing!”
Read in context →The cheerful "good thing!" does all the arguing by assertion. One loop keyword is defensible, but the reader deserves the reason — fewer ways to say the same thing — not a pat on the head.
“Keep the discipline! You don't need to know anything new right now to make the test fail properly.”
Read in context →The real lesson of the whole book, hiding in a throwaway line: a stub exists to prove the test can fail, not to approximate the answer.
“repeat_test.go:10: expected 'aaaaa' but got”
Read in context →Small slip — the test used %q, so the output would carry double quotes, not single. A tiny thing, but this chapter's whole pedagogy is "trust the output you see."
6 more notes
“is simply short hand for both steps. Here we are declaring a string variable only”
Read in context →Good catch by the author, but the actual reason is left unsaid: you want the zero value "" to accumulate into, and := demands something to infer a type from.
“After Loop() returns false, b.N contains the total number of iterations that ran.”
Read in context →This is the modern b.Loop() form replacing the old i < b.N idiom — worth noticing that the fix was moving the count from input to output, so the framework owns the timing boundary.
“The number of times the code is run shouldn't matter to you”
Read in context →It shouldn't — until it does. A function with per-call setup cost or cache effects benchmarks very differently at 10 iterations vs 10 million, and "don't look" is how people ship misleading numbers.
“strings.Builder[stringsBuilder] type which minimizes memory copying”
Read in context →A mangled artifact of the text, but it points at something real: Builder isn't magic, it just keeps a growable []byte and hands it over at the end without a copy.
“10000000 25.70 ns/op 8 B/op 1 allocs/op”
Read in context →5x faster and one allocation — for five characters. Note the honest caveat the chapter skips: at this size the win is real but tiny in absolute terms; Builder earns its keep as the loop count grows.
“Write ExampleRepeat to document your function”
Read in context →Quietly the best exercise here. Example functions are compiled and run by go test, so they're the only documentation in the language that cannot silently rot.
A reader’s takeAI reading companion
Transcription scars in a book whose pedagogy is reading carefully
A deliberately small chapter doing three jobs at once: teach the one loop keyword, rehearse the red-green-refactor rhythm with almost no new syntax, and sneak in benchmarking as if it were ordinary. The pedagogical move I admire is the stub returning "" — the author insists you watch the test fail for the right reason before writing a line of real logic, and that discipline is the actual subject, not for. The strings.Builder turn is the chapter's one piece of genuine engineering content, and it is handled with a nice empirical honesty: don't claim it's faster, measure it. Where I'd push back: the cheerleading ("Which is a good thing!", "pretty ok!") substitutes for reasoning in a couple of places, the claim that iteration count "shouldn't matter to you" is true only for well-behaved functions, and a 5x speedup on a five-character string is a real but modest result being presented as a revelation. The text also carries small transcription scars — a %q output shown with single quotes, a garbled strings.Builder mention — which matter more than usual in a book whose method is "read the output carefully."
Read Go fundamentals with these cards beside each passage.
Open in FeedLitraGo fundamentals: Arrays and slices
How does append decide when to grow the underlying array?
How Go fundamentals unfoldsAI-assisted
Opening: arrays as the simplest case. The chapter begins by framing arrays as ordered, same-type collections, then immediately puts the book's TDD loop to work on a trivially concrete goal: a Sum function that totals a collection of numbers. The test comes first, with a five-element integer array and an expected total of 15, and the author takes the opportunity to teach small details as they arise — the two initialization forms (explicit size versus [...]), the %v verb for printing inputs in failure messages, and a practical aside about why go mod init main breaks test imports and how renaming the module fixes it. From there the loop runs through its stages exactly as advertised: a stub returning 0 to confirm a meaningful failure, an index-based for loop to make it pass, and a refactor to range, which introduces the index/value pair and the _ blank identifier.
First turn: the limitation of arrays motivates slices. The chapter then names the catch it has been building toward — an array's size is part of its type, so [4]int and [5]int are as incompatible as string and int. This sets up slices as the practical alternative, and the author stages the transition as a design decision rather than a syntax lesson: with a new test for "a collection of any size," you must either break the existing API or add a second function. Choosing the former (no external callers exist), the fix turns out to be nothing more than changing the parameter type and letting the compiler point at the remaining breakage. The refactor stage is then aimed at the tests rather than the code, and becomes a short argument about testing philosophy: tests have a maintenance cost, confidence rather than count is the goal, and the two sum tests are redundant. The go test -cover tool appears here as a way to find untested code — with the caveat that 100% coverage is a byproduct of disciplined TDD, not a target in itself.
Development: variadics, append, and comparing slices. The next requirement, SumAll, returns one total per slice passed in, which brings in variadic parameters (...[]int). Writing the test exposes a genuine obstacle: Go forbids == on slices, so the chapter introduces slices.Equal from the standard library (Go 1.21+), noting its shallow-comparison limits for non-comparable element types. The first passing implementation uses make to preallocate, which lets the author distinguish length from capacity; the refactor replaces it with append, which sidesteps capacity management entirely and reads more simply.
Second turn: a runtime error, and tightening the tests. SumAll becomes SumAllTails, introducing the slice operator slice[low:high] and the head/tail distinction. The author then deliberately asks what the tail of an empty slice is — and the resulting test compiles cleanly but panics with an out-of-range slice bound. This is the chapter's sharpest point: compile-time errors are allies because they catch mistakes before shipping, while runtime errors reach users. A length guard fixes it. The closing refactor extracts the repeated assertion into checkSums, but assigns it to a variable rather than declaring a package-level function — demonstrating that functions are values, that closures can capture local scope, and that keeping helpers unexported narrows your API surface while still giving you type safety at the call site.
Close. The wrap-up inventories what was covered — arrays, slices and their construction, append, slicing slices, len, the coverage tool, and slices.Equal — then points outward: these types work with any element type, including nested slices like [][]string. Readers are directed to the Go blog's slices article, the Go playground, and prepared examples showing how a slice shares its backing array with the original (and why copying a slice taken from a very large one is often wise) — a hint at the aliasing behaviour the chapter itself only brushes against.
Key claims, with the lines that support them
Fixed-length arrays are a poor fit for general-purpose functions, so slices are the normal choice.
The author writes Sum against a five-element array, then adds a test passing a three-element slice; the code refuses to compile with a message saying a slice cannot be used where a five-element array is expected. Only after the parameter becomes a slice do both cases work, which is what motivates the move away from arrays.
“If you try to pass an [4]int into a function that expects [5]int, it won't compile.”
Arrays and slices › Arrays and their typeRead in context →
When nobody else depends on a function, it is better to change its signature than to maintain a second near-duplicate function.
Facing the slice-versus-array compile failure, the author lays out the two options — break the existing shape of Sum, or add a parallel function — and notes that changing a published signature would ruin the day of any caller. Because this function has no outside users, the cost of the break is nil while two functions would mean double maintenance forever, so the single slice version wins; the price paid is rewriting the first test.
“If you try to pass an [4]int into a function that expects [5]int, it won't compile.”
Arrays and slices › Arrays and their typeRead in context →
The aim is confidence in the code, not a large number of tests, because every test carries upkeep.
Having written one test for a five-element slice and another for a three-element one, the author judges the pair redundant: if the sum works for one size it almost certainly works for any size within reason. The caveat is that tests are the only coverage signal — deleting one and re-running the coverage report shows the drop — so the trade is maintenance overhead against information, not tests against nothing.
“It should not be a goal to have as many tests as possible, but rather to have as much confidence as possible in your code base.”
Arrays and slices › RefactorRead in context →
2 more claims
Errors caught by the compiler are preferable to errors that surface while the program runs.
The empty-slice test compiles cleanly and then panics with a slice-bounds-out-of-range failure, which the author uses to draw the contrast: a compile error merely stops the author, while a runtime error is what actually reaches users. This is why the fix is an explicit length check rather than hoping callers never pass empty input.
“Compile time errors are our friend because they help us write software that works, runtime errors are our enemies because they affect our users.”
Arrays and slices › Try and run the testRead in context →
Defining a test helper as a local variable inside the test keeps it out of the package's surface and buys type checking for free.
Extending the previous point about compile-time safety, the author assigns the repeated assertion to a function-valued variable inside the test. Because it lives in that scope, nothing else in the package can call it; and because its parameters are typed slices, a mistaken call passing the string "dave" is rejected by the compiler with a type mismatch instead of failing later.
“Compile time errors are our friend because they help us write software that works, runtime errors are our enemies because they affect our users.”
Arrays and slices › Try and run the testRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Size in the type blocks reuse
An array's length is baked into its type, so a function declared for a five-element array simply has a different parameter type from a four- or three-element one and the compiler refuses the call. Slices leave the size out of the type, so one function signature accepts collections of any length — which is why the chapter switches to them.
Mechanism 2
Why append beats manual sizing
A slice carries a length and an underlying capacity, and assigning to an index past that capacity blows up while the program is running rather than at compile time. append sidesteps the issue by taking a slice plus a value and handing back a slice that already holds everything, so the code never has to reason about how much room is left.
Mechanism 3
Slicing an empty slice panics
Asking for everything after the first element of a collection that has no elements is an out-of-range request, and Go can only discover that once the values exist, so the test compiles and then panics. Guarding with a length check and contributing zero for empty input converts the crash into defined behaviour.
Common misreadings
Since the chapter shows a coverage report reading 100%, high coverage is the target to aim at and more tests mean better code.
The coverage tool is presented as a way to spot code no test touches, while the real target is confidence; the author says striving for full coverage should not be the end goal and that disciplined test-first work tends to produce near-complete coverage as a side effect. Extra tests that add no confidence — like a second sum test at a different size — are counted as pure maintenance cost.
“It should not be a goal to have as many tests as possible, but rather to have as much confidence as possible in your code base.”
Arrays and slices › RefactorRead in context →
Slices behave like arrays or other values, so two of them can be compared with the equality operator in a test.
Go rejects equality operators on slices — the compiler reports that a slice can only be compared to nil — so the chapter reaches for the standard library's slices.Equal, available from Go 1.21, which does a shallow element-by-element comparison. That function requires comparable elements, so it does not apply to things like two-dimensional slices.
“Go has slices which do not encode the size of the collection and instead can have any size.”
Arrays and slices › Arrays and their typeRead in context →
Key concepts in Go fundamentals
- Slice
- A variable-length collection built on an array, declared without a size.
- Where it applies: Cannot be compared with == ; needs slices.Equal and comparable elements.
- range
- Loop form that yields index and value on each iteration.
- Where it applies: Index can be dropped with the blank identifier _.
- Variadic function
- A function that accepts a varying number of arguments, like SumAll.
- Where it applies: Arguments arrive as a slice inside the function.
- Slice capacity
- How many elements the underlying array can hold, distinct from length.
- Where it applies: Indexing beyond capacity panics at runtime; append grows safely.
- Test coverage tool
- go test -cover reports which statements tests exercise.
- Where it applies: 100% coverage is not the goal; confidence is.
0 more concepts
Reader’s notes on Go fundamentalsAI reading companion
“package main will only contain integration of other packages and not unit-testable code”
Read in context →This is presented as Go gospel, but it's really a convention dressed as a rule. Plenty of real projects put testable code in main — the compiler restriction is about importing, not about where logic belongs.
“for i := 0; i < 5; i”
Read in context →The hardcoded 5 is the whole lesson hiding in plain sight. The size lives in three places — type, loop, test — and that triplication is exactly what slices will dissolve.
“the size is encoded in its type”
Read in context →The single most surprising fact about Go arrays, and the author drops it in one line. [4]int and [5]int being unrelated types is why nearly nobody uses arrays in Go.
9 more notes
“we will potentially ruin someone's day because our other test will no longer compile”
Read in context →Nice move: the API-breaking decision is framed socially, not technically. And then the escape hatch — "no one else is using our function" — is the honest admission that this freedom is a luxury of toy code.
“It should not be a goal to have as many tests as possible”
Read in context →The most valuable paragraph in the chapter, and it slightly undercuts TDD orthodoxy from inside. Deleting a test you just wrote takes more discipline than writing it.
“coverage: 100.0% of statements”
Read in context →Worth noticing what this number can't see: Sum had zero branches, so 100% here is nearly free. The author hedges honestly, but the metric still flatters.
“it can't be applied to slices with non-comparable elements like 2D slices”
Read in context →A quiet trapdoor. The chapter later cheerfully suggests [][]string, without reconnecting that you've just lost slices.Equal for it.
“In this implementation, we are worrying less about capacity.”
Read in context →"Worrying less" is doing some work here. append on a nil slice reallocates as it grows; make with a known length didn't. The refactor trades a little performance for a lot of clarity — a fair trade, but it is a trade.
“Compile time errors are our friend”
Read in context →The chapter earns this line rather than asserting it — it just let a passing compile blow up at runtime first. Pedagogically the strongest beat in the whole piece.
“if len(numbers) == 0”
Read in context →Note the fix encodes a decision, not a fact: the tail-sum of an empty slice is 0 only because we said so. Returning an error, or skipping it, were equally defensible. Tests freeze opinions into behavior.
“assigning a function to a variable”
Read in context →Sold as a type-safety and scoping win, but there's a cost the author skips: a closure can't be reused across test functions, so the next test file re-invents it. The "reduced API surface" is also reduced reuse.
“changing the slice affects the original array; but a "copy" of the slice”
Read in context →The genuinely dangerous thing about slices — shared backing arrays — gets one sentence at the very end, as a link. This deserved its own red-failing test.
A reader’s takeAI reading companion
Backing arrays and aliasing create real slice bugs but barely get examined
A well-paced chapter whose real subject isn't slices but the discipline of letting the compiler and the test drive design. Its best moves are structural: the hardcoded 5 planted early so slices can later remove it; a runtime panic staged deliberately so the "compile errors are friends" line lands as evidence rather than slogan; and — most unusually for a TDD text — an argument that you should delete tests, that coverage is diagnostic rather than a target. That self-critique is the chapter's intellectual high point. The gaps are mostly about what slices share rather than what they hold. Aliasing and backing arrays, the actual source of production slice bugs, arrive in a closing sentence and an outbound link, while the append-vs-make refactor is framed as "worrying less about capacity" without naming the reallocation it accepts.
The slices.Equal caveat about non-comparable elements is stated, then quietly contradicted by the breezy [][]string suggestion at the end. And the empty-slice fix is presented as making a test pass when it is really an API decision — 0 is a convention, not a discovered truth; the test merely freezes it. Read against the grain, the chapter is a nice demonstration that TDD doesn't tell you what the right answer is, only that you must commit to one in public.
Read Go fundamentals with these cards beside each passage.
Open in FeedLitraGo fundamentals: Structs, methods & interfaces
How do pointer receivers change what a method can do?
How Go fundamentals unfoldsAI-assisted
The chapter starts almost disarmingly small — a request for geometry code that computes a rectangle's perimeter from a width and a height. The author deliberately leans on familiarity: the TDD cycle is assumed known by now, so the opening beats (write the failing test, see the "undefined: Perimeter" compiler complaint, return 0, then return the real formula) move quickly and serve mainly to introduce incidental details like the %.2f format verb. The reader is then handed Area as a self-directed exercise, which establishes the chapter's rhythm: the author demonstrates, then asks you to repeat the loop yourself.
Before moving on, there's a short aside on floating-point comparison — the familiar warning that == on floats is risky, with the twist that the literal expression 0.1+0.2 == 0.3 prints true while the same sum through variables prints false, because Go evaluates untyped constant expressions at arbitrary precision. The point of the digression is reassurance rather than alarm: because the chapter's test values are whole numbers that float64 represents exactly, exact comparison is legitimate here; the tolerance-based approach (go-cmp's EquateApprox) is flagged as the tool for when it isn't.
Development: from loose parameters to named types
The first real turn is a refactor motivated by meaning, not correctness. The functions work but say nothing about rectangles, so someone could feed them triangle dimensions and get a plausible-looking wrong answer. Rather than renaming to RectangleArea, the author introduces the struct — a named bundle of fields — and rewrites both tests and functions around a Rectangle type. The compiler's "not enough arguments" message is used as a teaching device, reinforcing the habit of running the test before fixing anything.
Adding circles is what forces the next conceptual step. Once Circle exists, Area(Circle) and Area(Rectangle) can't coexist — Go has no function overloading, and the compiler says so plainly. The author weighs putting them in separate packages, rejects it as overkill, and pivots to methods: a function with a receiver, invoked on an instance. The tests are rewritten to rectangle.Area() and circle.Area() first, then the receiver syntax func (r Rectangle) Area() float64 is introduced, along with the convention of naming the receiver after the type's first letter and the observation that Go's explicit receiver replaces the implicit this of other languages. Stub implementations returning 0 get things compiling; r.Width * r.Height and math.Pi * c.Radius * c.Radius make them pass.
The pivot to interfaces, and then to test design
The chapter's central idea arrives through test duplication. Wanting a checkArea helper that accepts both shapes but rejects non-shapes at compile time, the author writes the helper against a Shape parameter — and the tests pass the moment a three-line interface declaring Area() float64 is added. The "Wait, what?" beat is the chapter's real payoff: no implements keyword, no declaration of intent from the concrete types. Satisfaction is structural and implicit, and the lesson drawn is about decoupling — the helper never learns whether it holds a rectangle or a circle, only that it can ask for an area.
From there the focus shifts from language features to test craft. Table-driven tests arrive as an application of structs: an anonymous []struct of shape-and-want pairs, iterated like any slice. A Triangle is added to demonstrate how cheap extension becomes, again letting the compiler walk the reader from "undefined: Triangle" through "does not implement Shape" to a passing (base * height) * 0.5. Then the table itself is refined in stages — named struct fields instead of bare positional values, %#v so failures print the offending struct, want renamed to hasArea, and each case wrapped in t.Run with a name so output identifies the failing case and go test -run TestArea/Rectangle works. A Kent Beck quote frames the goal: tests should read as assertions of truth, not sequences of operations. The author is careful to add a caveat — tables earn their extra noise only when you're genuinely testing many implementations or input variations.
Close
The wrap-up enumerates the four acquisitions — structs, interfaces, methods, table-driven tests — and reframes them as a milestone: this is the chapter where the reader begins designing their own types, which in a statically typed language is the foundation of code that's easy to understand, assemble, and test. The final note points forward, promising that the same implicit-satisfaction trick that made Shape work will let the reader's own types plug into widely used standard-library interfaces and inherit a great deal of functionality for free.
Key claims, with the lines that support them
Wrapping width and height in a Rectangle type says what the code is for in a way two bare float64 parameters cannot.
The author points out that a function taking two plain numbers happily accepts a triangle's base and height and returns a confidently wrong answer; nothing in its signature mentions rectangles. Renaming the function would help, but defining the type encapsulates the idea once and makes every call site state its intent.
“An unwary developer might try to supply the width and height of a triangle to these functions without realising they will return the wrong answer.”
Structs, methods & interfaces › RefactorRead in context →
Since Go won't let you reuse one function name for different types, attaching behaviour to the types as methods is the natural way out.
Writing both Area(Circle) and Area(Rectangle) in one package fails to compile with a redeclaration error — something several other languages would allow. The author notes you could put each in its own package, calls that overkill, and instead gives each type its own Area method with a receiver, after which rectangle.Area() and circle.Area() coexist.
“We can define methods on our newly defined types instead.”
Structs, methods & interfaces › Write the minimal amount of code for the test to run and check the failing test outputRead in context →
Declaring an interface with only the one method you actually need decouples a function from the concrete types it serves.
Following on from the method step, the duplicated assertions collapse into a single checkArea helper that asks for a Shape. It never learns whether it holds a rectangle, circle or triangle — only how to ask for an area — yet passing a non-shape still fails at compile time. The author flags that the full design payoff comes in later chapters.
“By declaring an interface, the helper is decoupled from the concrete types and only has the method it needs to do its job.”
Structs, methods & interfaces › DecouplingRead in context →
2 more claims
Comparing floats with == is acceptable precisely when every value involved is exactly representable.
The author contrasts two lines: 0.1+0.2 == 0.3 written as literals reports true because Go evaluates constant expressions with arbitrary precision, while the same sum through float64 variables reports false. He then explains why the area tests are unaffected — 12, 6 and 72 are whole numbers — and recommends an approximate comparison such as go-cmp's EquateApprox once inexact values or division enter.
“Exact comparison only becomes unsafe once you introduce values or calculations that aren't exactly representable, like 0.1, or results of division.”
Structs, methods & interfaces › Write enough code to make it passRead in context →
Table-driven tests only pay off if each case is readable and each failure identifies itself.
A bare list like {Rectangle{12, 6}, 72.0} leaves the reader guessing what the numbers mean, so the author names the struct fields, renames want to hasArea, prints the case with %#v, and wraps each case in t.Run. A failure then reports FAIL: TestArea/Rectangle with the struct's field values, instead of an anonymous got/want among twenty cases — and that case can be rerun alone via go test -run.
“It's not immediately clear what all the numbers represent and you should be aiming for your tests to be easily understood.”
Structs, methods & interfaces › RefactorRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Methods alone decide interface fit
An interface in Go just lists the methods a caller needs. The compiler checks whether a type already has those methods and, if so, accepts it — so adding a four-line Shape interface made both Rectangle and Circle usable with no change to either type, and the helper stayed type-safe while losing all knowledge of concrete shapes.
Mechanism 2
Why whole-number equality is safe
float64 stores whole numbers exactly, and multiplying two exactly stored numbers gives another exact result as long as it stays in range, so comparing with == is reliable there. Trouble starts only with values like 0.1 or with division, where the stored value already differs from the one you wrote.
Mechanism 3
Compile errors as the next step
Each test is written against code that doesn't exist yet, so the compiler names exactly what's missing — an undefined type, then a missing method, then a wrong value. The author treats reading those messages as the design loop itself: you add the smallest thing the error asks for, which is how the struct, then the method, then the real formula appear.
Common misreadings
A type has to announce somewhere that it implements an interface, the way you write "implements" in other languages.
Go resolves this implicitly: any type whose methods match what the interface asks for satisfies it, checked by the compiler. Rectangle and Circle became Shapes the moment they had an Area() float64, with nothing added to their declarations; string isn't one simply because it lacks that method.
“By declaring an interface, the helper is decoupled from the concrete types and only has the method it needs to do its job.”
Structs, methods & interfaces › DecouplingRead in context →
Floating-point numbers should never be compared with == in tests, so the area assertions here are a latent bug.
The hazard comes from values that float64 can't store exactly, not from the operator. Whole numbers like 12, 6 and 72 are stored exactly and their product stays exact, so equality holds; the time to switch to a tolerance-based comparison is when inputs like 0.1 or divisions appear.
“Exact comparison only becomes unsafe once you introduce values or calculations that aren't exactly representable, like 0.1, or results of division.”
Structs, methods & interfaces › Write enough code to make it passRead in context →
Key concepts in Go fundamentals
- Struct
- A named bundle of fields that stores related data together and makes intent explicit.
- Where it applies: Only groups data; behaviour must be added separately as methods.
- Method
- A function with a receiver, called on an instance of a type rather than anywhere.
- Where it applies: Needs an instance of the receiver's base type; cannot be called standalone.
- Interface
- A declaration of the methods you need, which any type with those methods satisfies automatically.
- Where it applies: Satisfaction is implicit and checked at compile time; no declaration of intent is written.
- Table driven tests
- A slice of case structs iterated in one loop so new cases are cheap to add.
- Where it applies: Worth the extra noise mainly for several implementations of an interface or varied inputs.
- TDD cycle
- Write a failing test, let the compiler guide you, make it pass, then refactor.
- Where it applies: Here each round is driven by a compiler error rather than a design decision made up front.
0 more concepts
Reader’s notes on Go fundamentalsAI reading companion
“The second comparison is false because 0.1 and 0.2 can't be represented exactly”
Read in context →This digression is the chapter's most honest passage — most tutorials wave at float comparison as a taboo. Here the rule gets an actual reason, plus the condition under which you may safely ignore it.
“An unwary developer might try to supply the width and height of a triangle”
Read in context →The real motivation for types isn't elegance, it's the next person's mistake. Nice that the refactor is justified by a failure mode rather than taste.
“the f has been replaced by g, with good reason”
Read in context →A tiny formatting choice treated as a design decision: a test that lies about its numbers is worse than no test. Easy to skip, and worth not skipping.
6 more notes
“Area redeclared in this block”
Read in context →The author lets Go's lack of overloading sting before offering methods. Notice the rhetorical move: a language limitation gets reframed as motivation for the idiomatic feature.
“In Go interface resolution is implicit.”
Read in context →The payoff of the whole chapter, and delivered almost offhandedly. Also the part that unsettles newcomers most — nothing anywhere declares the relationship, so the compiler's silence is the only confirmation.
“Once you add this to the code, the tests will pass.”
Read in context →Writing a type declaration and having previously-failing code compile feels like magic until you see it's just the compiler checking method sets. Good that "Wait, what?" follows immediately.
“but be sure that you have a need for the extra noise in the tests”
Read in context →A rare moment of restraint in a book teaching a technique. Table tests are the chapter's trophy, and the author still warns against reaching for them reflexively.
“rename the want field into something more descriptive like hasArea”
Read in context →Renaming want to hasArea turns a test row into a sentence. Small, and the clearest demonstration of Beck's "assertion of truth" claim the chapter makes.
“the helper is decoupled from the concrete types and only has the method it needs”
Read in context →"Declare only what you need" is the actual Go lesson here — interfaces defined by the consumer, not the implementer. It's dropped in one paragraph and deferred to later chapters, which undersells it.
A reader’s takeAI reading companion
Go's implicit interfaces get one sentence, table test formatting gets whole chapters
A deceptively well-built chapter: it uses one trivial problem — area of a shape — to walk the whole arc from free functions to structs to methods to implicit interfaces, and every step is motivated by a concrete discomfort rather than by a feature tour. The strongest teaching move is letting the compiler do the arguing: each error message is quoted and read aloud, so the reader learns to treat compiler output as instruction rather than obstacle. The float-comparison digression is unusually honest for an introductory text, giving a reason and a boundary condition instead of a superstition. The weakest spot is proportion: structural typing — the genuinely distinctive idea, where implementing an interface requires saying nothing at all — lands in a single sentence, and the consumer-defines-the-interface principle that makes Go's design philosophy cohere is mentioned and deferred.
By contrast, table-driven test cosmetics get several pages. Defensible for a beginner's sequence, but it means a reader can finish the chapter fluent in test formatting and only dimly aware of why Go's interfaces matter. The closing restraint — table tests are noise unless you need them — is the sign of an author who has watched his own advice get overapplied.
Read Go fundamentals with these cards beside each passage.
Open in FeedLitraGo fundamentals: Pointers & errors
When is a value receiver actually the better choice?
How Go fundamentals unfoldsAI-assisted
The chapter opens in a deliberately light register — fintech, Bitcoin, "what an amazing banking system we can make" — and uses that framing to move from the previous chapter's structs toward the idea of managed state: a Wallet whose balance is hidden and reachable only through methods. The TDD cycle runs as usual (undefined type, undefined methods, a failing assertion), and the author pauses to note Go's visibility rule: a lowercase identifier is private to its package, which is exactly what makes balance "secure." Then comes the chapter's first real teaching moment, staged as a surprise: the code looks correct, Deposit adds to w.balance, yet the test still reports got 0 want 10.
The diagnosis is the heart of the opening half. Go copies arguments, so the method's receiver w is a copy of the caller's wallet. Rather than assert this, the text has you print &wallet.balance in both places and observe two different addresses — the mutation happens on a throwaway copy. The fix is a pointer receiver, *Wallet, and the author then answers the obvious follow-up: why does w.balance work without writing (*w).balance? Because Go auto-dereferences struct pointers; the explicit form is valid but was judged cumbersome. A convention note closes the thread — Balance doesn't technically need a pointer receiver, but keep receiver types consistent.
A refactor turn follows, shifting from mechanics to expressiveness. int works but says nothing, so the chapter introduces type definitions (type Bitcoin int), shows that methods can be declared on them, and implements fmt.Stringer so %s prints "10 BTC". There's a small bit of honest self-correction here: the author admits adding String without a test first, notes that go test -cover would have flagged it, and goes back to write the test — while cautioning against chasing coverage for its own sake. Withdraw arrives next through the same loop, and test duplication is factored into an assertBalance helper.
The second half pivots from pointers to errors, prompted by a requirement question: what if you withdraw more than you hold? Go's idiom is to return an error for the caller to check, so the test is written to expect a non-nil error and an unchanged balance. This pulls in a short digression on nil — interfaces are nillable, dereferencing nil panics, so check for it — and a compiler nudge (used as value) forces the signature change. The error message then gets iterated: errors.New("oh no") becomes a real message, t.Fatal guards the helper against calling .Error() on nil, and finally the chapter makes its key point about errors being values: the duplicated message string is extracted into a package-level ErrInsufficientFunds sentinel, so tests compare values rather than wording. The author adds a nice meta-observation — tests reveal how your API will actually feel to use. A detour to the errcheck linter catches the one untested case: the successful withdrawal never verified that no error came back, yielding assertNoError.
The closing stretch extends the error story outward. Sentinel comparison with == works only while one function can produce the error; in layered programs it loses context, so the chapter introduces fmt.Errorf with %w to wrap an error inside a message that names the account and operation. That wrapping breaks ==, which motivates errors.Is, explained as repeatedly unwrapping until it matches or runs out — a safe default even for unwrapped errors — with errors.As mentioned as the typed-extraction counterpart, deferred to a later chapter. The wrap-up then collects the chapter's threads under headings: pointers for mutation and for avoiding copies of large or single-instance things, nil as both a hazard the compiler won't catch and a way to express absence, errors as return values best expressed as meaningful values rather than strings, and new types as a tool for domain meaning and interface satisfaction — ending with the reassurance that the compiler will usually guide you if you read it carefully.
Key claims, with the lines that support them
If a method has to change a struct's state, it must take a pointer to that struct.
The author's deposit method looked correct but the balance stayed at zero. Printing the memory address of the balance field inside the test and inside the method showed two different addresses, proving the method was editing a copy; switching the receiver to a pointer made the test pass. Read-only methods don't strictly need this, though the author keeps receivers uniform for consistency.
“So when we change the value of the balance inside the code, we are working on a copy of what came from the test.”
Pointers & errors › That's not quite rightRead in context →
Tests should compare errors as values rather than asserting on their wording.
The chapter first writes a helper that compares the error's text against the literal "cannot withdraw, insufficient funds", then notices the same sentence now lives in both the test and the implementation — so merely rephrasing the message would break the test, even though nothing about the behaviour changed. Pulling the error into a package-level variable gives one source of truth, and the test asserts against that variable. Listening to the awkward test is what drove the better design: callers of the API can now compare against the same value.
“It would be really annoying for the test to fail if someone wanted to re-word the error and it's just too much detail for our test.”
Pointers & errors › RefactorRead in context →
In layered code you should add context to an error while preserving the identity of its cause.
Building on the sentinel-error design, the author points out its limit: if a wallet sits under a bank which sits under a request handler, and every layer returns the error untouched, the top only ever learns "insufficient funds" without knowing which account or operation failed. A processing function that wraps the wallet's error with the account id prints both the context and the original cause in one message. Because wrapping produces a new value, a plain equality check against the sentinel would now fail, so the chain-walking matcher is used instead — it also works on errors that were never wrapped, making it the safe default. When the goal is pulling a typed error out of the chain rather than matching a known value, the companion extraction function is the right tool.
“This is exactly the problem errors.Is solves - it checks a chain of wrapped errors, not just the outermost one:”
Pointers & errors › Checking wrapped errorsRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Why a copied receiver loses your edit
Go copies arguments at call time, so a method declared on the plain struct type works on its own duplicate of the wallet. Adding to that duplicate's balance leaves the caller's wallet untouched, which is why the balance read back is still zero. Declaring the receiver as a pointer passes the address instead, so the method edits the original.
Mechanism 2
How a wrapped error keeps its cause
Formatting an error with %w does more than paste its text into a new message: it stores the original error inside the new one and gives the result a method for handing it back out. A matcher can therefore peel the outer layers off one at a time until it finds the error you asked about, so added context never hides the underlying cause.
Mechanism 3
A named type can teach printing
Declaring a new type from an existing one lets you hang methods on it. Defining a String method makes the type satisfy the printing interface from the formatting package, so %s renders it in domain terms instead of as a bare number — test failures then read as amounts of currency rather than ambiguous integers.
Common misreadings
With a pointer receiver you have to write (*w).balance to reach a field, otherwise you're touching the pointer rather than the value.
Both forms are legal, and the explicit version is correct — but Go's designers found it clumsy, so pointers to structs are dereferenced automatically and w.balance means exactly the same thing.
“The difference is the receiver type is *Wallet rather than Wallet which you can read as "a pointer to a wallet".”
Pointers & errors › That's not quite rightRead in context →
Once errors are package-level values, checking them with == is all you ever need.
Equality only matches the outermost error. As soon as an outer layer wraps the cause with extra context, the returned value is a different error, so the chain-walking check is needed to still recognise the original cause.
“This is exactly the problem errors.Is solves - it checks a chain of wrapped errors, not just the outermost one:”
Pointers & errors › Checking wrapped errorsRead in context →
Key concepts in Go fundamentals
- Pointer receiver
- Writing (w *Wallet) so a method edits the original struct instead of a copy.
- Where it applies: Not needed for read-only methods, but kept for consistency by convention.
- Sentinel error value
- A package-level error variable callers compare against instead of matching message strings.
- Where it applies: Direct == comparison breaks once the error is wrapped by an outer layer.
- Error wrapping with %w
- fmt.Errorf with %w stores the original error inside a new one carrying extra context.
- Where it applies: %v only copies the message; only %w keeps the chain inspectable.
- Type declaration from existing type
- type Bitcoin int creates a named type that can carry its own methods and meaning.
- Where it applies: Behaves like the underlying type; value is in domain meaning and interface support.
0 more concepts
Reader’s notes on Go fundamentalsAI reading companion
“Fintech loves Go and uhhh bitcoins?”
Read in context →The deliberate shrug is doing work: it signals the domain is a costume, so you don't waste attention on banking realism.
“run the test suite and bask in the passing test”
Read in context →The setup for the chapter's best joke — the book lets you feel competent one line before pulling the rug. Failure as pedagogy.
“the addresses of the two balances are different”
Read in context →This is the whole chapter. Rather than assert "Go copies values," it makes you print two numbers and notice they disagree. Evidence beats doctrine.
9 more notes
“the makers of Go deemed this notation cumbersome”
Read in context →Honest about the sugar. Most tutorials hide automatic dereferencing and leave learners mystified about when a pointer "acts like" a value.
“you should keep your method receiver types the same for consistency”
Read in context →Convention smuggled in right after a technical claim it contradicts. Worth asking: consistency for whom? The real reason is that a mixed receiver set makes the value/pointer distinction leak at call sites.
“We lacked discipline here: we've added a method without writing a test”
Read in context →The confession is more instructive than the method. And note the refusal of 100% coverage as a goal — coverage as a prompt to think, not a target.
“it is very important to just write enough code to satisfy the compiler”
Read in context →In Go the compiler is a test runner of sorts; "red" often means "won't build." The TDD loop quietly absorbs type errors as a first-class failure state.
“It would be really annoying for the test to fail if someone wanted to re-word”
Read in context →The best argument in the chapter: let the test's pain tell you what the API should be. Sentinel errors aren't justified by style but by test fragility.
“we have not checked that if the Withdraw is successful that an error is not returned”
Read in context →Sharp catch — the untested case is the happy path's silence. A linter finding a hole in a TDD'd file is a nice admission that discipline alone isn't coverage.
“all a caller several layers up ever sees is insufficient funds”
Read in context →This is the real motivation for wrapping, stated as a loss of information rather than a feature list. Sentinels scale badly across layers, and the chapter admits it.
“it wraps err inside the new error fmt.Errorf returns”
Read in context →The %v/%w contrast is the right pivot: same printed string, different structure. Everything about errors.Is follows from that one difference.
“it's a safe default that also works for errors that were never wrapped”
Read in context →Quietly the most actionable line here — just use errors.Is always, since == buys nothing and breaks the moment anyone adds context upstream.
A reader’s takeAI reading companion
Even test-first code can hide untested paths from humans
A chapter with two halves that don't quite acknowledge each other. The first half is the book at its best: instead of asserting that Go copies values, it hands you two memory addresses and lets the contradiction land, so the pointer receiver arrives as a remedy you already wanted. The error half follows the same method — the "oh no" string is deliberately bad, the string-comparing assertion is deliberately brittle, and the sentinel ErrInsufficientFunds emerges as something the test demanded rather than something the author decreed. That is the chapter's real lesson, more than pointers or errors: let the discomfort of your tests dictate your API.
Two things press on it. First, the wrapping section, clearly a later addition, undercuts the equality check the earlier pages just taught — and to its credit says so, landing on errors.Is as the safe default. A reader who stops before that section walks away with advice that silently breaks as soon as anyone adds context upstream. Second, the errcheck moment is a small, honest crack in the TDD story: a tool found an untested path in code written test-first, because the happy path's absence of an error is easy to forget to assert. The chapter's confession about writing String() without a test, and its explicit refusal to chase 100% coverage, belong to the same adult register — discipline as a practice you fall out of and return to, not a badge.
Read Go fundamentals with these cards beside each passage.
Open in FeedLitraGo fundamentals: Maps
Why can Delete mutate the dictionary without a pointer receiver?
How Go fundamentals unfoldsAI-assisted
Opening: the smallest possible question. The chapter arrives after arrays and slices, and frames maps as the complement to ordered storage — lookup by key rather than by position. Rather than survey map syntax abstractly, it picks a concrete vehicle, a dictionary, and starts with the most trivial behavior imaginable: given a map that already contains one word, searching for that word should return its definition. The first test is written before any implementation, and the explanation of map declaration syntax (map keyword, key type in brackets, value type after) is attached to that test rather than delivered as a preamble. One conceptual point is planted early and matters later: keys must be comparable, while values can be anything, including another map. The implementation cycle is almost comically short — a stub returning the empty string to confirm a clean failure, then a one-line return dictionary[word] — and the refactor step pulls assertion logic into an assertStrings helper. A second refactor converts the free function into a method on a named type, type Dictionary map[string]string, which is the structural move that makes everything afterward a method call.
First development: the absence problem and the two-value lookup. The chapter then asks what happens on a word that isn't there. The honest answer — you get the zero value, silently — is treated as acceptable but inferior, because the caller can't distinguish "no such word" from "word with no definition." This motivates the second return value and the error type. The tests are restructured into subtests (known word, unknown word), and the error assertion follows a specific discipline: check the error is non-nil first, then call .Error(). The implementation introduces the comma-ok idiom on map indexing, which the text presents as the mechanism that makes the distinction possible at all. The refactor then hoists the message into ErrNotFound and adds an assertError helper using errors.Is, with the stated payoff that tests no longer break when wording changes.
The turn: CRUD, and the recurring "but what if it already exists?" beat. From here the chapter settles into a rhythm it repeats three times. Add is written, passes with d[word] = definition, and then the author points out the flaw: maps overwrite silently, so a function named Add is quietly behaving like "set." The fix is to have Add consult Search and branch on the returned error via a switch — assigning on ErrNotFound, returning ErrWordExists on nil, and passing through anything unexpected in the default arm, which the text explicitly calls a safety net. Update repeats the pattern with the arms swapped and a new ErrWordDoesNotExist; the chapter pauses to defend the extra error variable rather than reusing ErrNotFound, arguing from a web-app example that distinct errors let callers redirect in one case and show a message in the other. Delete repeats it a third time, introducing Go's built-in delete, and by this point the test-then-stub-then-switch sequence is familiar enough that the prose compresses.
Two digressions that carry the real teaching. Interleaved with the CRUD cycle are two asides doing conceptual work the test cycle can't. The "Pointers, copies, et al" section corrects the natural inference from the fact that methods on a map value can mutate it without taking an address: a map value is a pointer to an internal structure, so passing one copies the pointer, not the data — it is not a reference type. That leads to the practical warning about nil maps, which read like empty maps but panic on write, and the rule to always initialize with a literal or make. The second aside, in the Add refactor, replaces errors.New variables with a DictionaryErr string type implementing Error(), so the sentinels can be declared as const — immutable and, per the cited argument, more reusable.
Close. The wrap-up is a plain inventory: a complete CRUD API for the dictionary, plus the enumerated skills — creating, searching, adding to, updating, and deleting from maps, and the error techniques (constant errors, error wrapping). The chapter's shape, then, is a single tiny read operation grown outward by repeatedly asking what the current implementation gets wrong, with the map-semantics and error-design asides dropped in at exactly the points where the code would otherwise mislead.
Key claims, with the lines that support them
A lookup function should say out loud that a value is absent instead of quietly handing back an empty result.
The author notes that returning nothing leaves the caller unable to tell whether the word is missing or merely has no definition, then rewrites Search to return a second error value, using the lookup's success boolean to return ErrNotFound. He concedes this matters little for a dictionary but can be decisive in other uses.
“This way, the user isn't left wondering if the word doesn't exist or if there is just no definition (this might not seem very useful for a dictionary.”
Maps › Write the test firstRead in context →
Pulling a hard-coded error message out into a named variable makes both the code and its tests sturdier.
The author replaces the inline message with ErrNotFound and a shared assertError helper that compares error values rather than text, so the tests keep passing if the wording is later reworded — which also shortens the test itself.
“This way, the user isn't left wondering if the word doesn't exist or if there is just no definition (this might not seem very useful for a dictionary.”
Maps › Write the test firstRead in context →
Since maps overwrite silently, an Add operation has to check first, or its name lies about what it does.
The author points out that storing under an existing key simply replaces the old value with no complaint, which is handy in general but wrong for something called Add. He writes a failing test on an already-present word, then has Add reuse Search and branch on its error: insert when not found, return ErrWordExists when found, and pass along anything unexpected — the switch acting as a safety net for errors he did not anticipate.
“Map will not throw an error if the value already exists. Instead, they will go ahead and overwrite the value with the newly provided value.”
Maps › RefactorRead in context →
3 more claims
Errors are better expressed as constants of a custom type than as ordinary variables.
As the number of error values grows, the author defines a string-based DictionaryErr type with an Error method so the error values can be declared as constants, arguing this makes them reusable and impossible to reassign.
“Map will not throw an error if the value already exists. Instead, they will go ahead and overwrite the value with the newly provided value.”
Maps › RefactorRead in context →
A failed update deserves its own error rather than being folded into the not-found error.
The author acknowledges he could reuse ErrNotFound but introduces ErrWordDoesNotExist instead, on the grounds that a precise error carries more information about what went wrong; his web-app illustration is that one case can trigger a redirect while the other shows a message to the user.
“This way, the user isn't left wondering if the word doesn't exist or if there is just no definition (this might not seem very useful for a dictionary.”
Maps › Write the test firstRead in context →
Update and Delete should reject words the dictionary does not contain, mirroring the guard Add needed.
Having fixed Add, the author shows the symmetric flaw: a bare assignment in Update would insert a brand-new word, and a bare delete call would quietly succeed on an absent key. Both are rewritten around the same Search-and-switch shape, only with the branches swapped so the write happens when the word exists and ErrWordDoesNotExist comes back when it does not.
“Map will not throw an error if the value already exists. Instead, they will go ahead and overwrite the value with the newly provided value.”
Maps › RefactorRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
How the two-value lookup tells apart missing and empty
A map read in Go can hand back a second boolean saying whether the key was actually present. Because of that extra signal, the dictionary can treat 'no such word' and 'word stored with an empty definition' as different situations, and return a dedicated error only in the first case.
Mechanism 2
Why passing a map still lets you change it
A map variable holds a pointer to the runtime structure that owns the data, so handing a map to a function copies only that pointer, not the buckets behind it. That is why methods on the dictionary can insert or delete entries without taking an address, even though Go passes everything by value.
Mechanism 3
Why a nil map is safe to read but fatal to write
A map declared without being given a value reads like an empty one, so lookups quietly succeed and hide the problem, but any write crashes the program at runtime. Creating the map up front with an empty literal or make points the variable at a real hash table, so writes always have somewhere to land.
Common misreadings
Since you can add to and delete from a map inside a function without passing a pointer, a map must be a reference type.
A map value is a pointer to the runtime structure holding the data, so what gets copied on each call is the pointer while the underlying table stays shared — the mutation works for that reason, not because Go treats maps as references.
“So when you pass a map to a function/method, you are indeed copying it, but just the pointer part, not the underlying data structure that contains the data.”
Maps › Pointers, copies, et alRead in context →
Declaring a map variable is enough to start using it, the way declaring a slice variable usually is.
A merely declared map is nil: reads behave like an empty map and give no warning, while the first write panics at runtime. Initialize it with an empty map literal or make so a real hash table exists before you store anything.
“A nil map behaves like an empty map when reading, but attempts to write to a nil map will cause a runtime panic.”
Maps › Pointers, copies, et alRead in context →
Key concepts in Go fundamentals
- Map
- A store that holds values under keys so you can look them up fast, like a word and its definition.
- Where it applies: Keys must be comparable types; a nil map panics on write.
- Comma ok lookup
- A map read can return a second boolean telling you whether the key existed at all.
- Where it applies: Only distinguishes missing key from present key, not empty value from error.
- Constant error type
- Defining your own string-based error type so error values can be declared as constants.
- Where it applies: Requires a custom type implementing Error(); plain errors.New values cannot be constants.
- Map copy semantics
- Passing a map copies only its internal pointer, so a function can still change the shared data.
- Where it applies: Feels like a reference type but is not; nil maps read as empty and panic on write.
0 more concepts
Reader’s notes on Go fundamentalsAI reading companion
“It can only be a comparable type because without the ability to tell if 2 keys are equal”
Read in context →Nice that the constraint is justified by mechanism rather than decree. Though "comparable" in Go has its own trapdoor: structs with slice fields look comparable until the compiler says no, and floats let NaN keys become unreachable.
“This is good because the program can continue to run, but there is a better approach.”
Read in context →The zero value as silent default is Go's most double-edged gift. The author treats it as a near-miss rather than a design philosophy — the real point is that absence and emptiness are indistinguishable, and only you know whether that matters.
“The second value is a boolean which indicates if the key was found successfully.”
Read in context →The comma-ok idiom quietly does what a whole Optional type does elsewhere. Worth noticing it's a syntactic special case, not something you could write yourself — Go gives the runtime privileges it denies your code.
8 more notes
“so our test doesn't fail if we change the error text in the future”
Read in context →This is the chapter's best transferable lesson and it's buried in a refactor aside: assert on identity, not on prose. Every test that string-matches an error message is a future false failure.
“A map value is a pointer to a runtime.hmap structure.”
Read in context →The honest framing. And it explains the asymmetry people trip on: you can mutate through the copy, but you can't make into it — assigning a fresh map inside a function is invisible outside.
“Therefore, you should never initialize a nil map variable:”
Read in context →Phrasing slip — var m map[string]string is precisely not initializing. And "never" is too strong: a nil map is a perfectly good read-only empty map, which is why nil slices and nil maps diverge in usefulness.
“switch err { case ErrNotFound:”
Read in context →Jarring after the test was just taught to use errors.Is. The implementation compares with == while the assertion tolerates wrapping — the book's own tooling has outgrown this code.
“Having a switch like this provides an extra safety net”
Read in context →It's a safety net for an impossibility. Search has exactly two outcomes, so default is unreachable — defensible as future-proofing, but calling it safety overstates it, and it's now an untestable branch.
“err := d.Search(word) switch err { case ErrNotFound: return ErrWordDoesNotExist”
Read in context →Three methods now share one shape with only the body swapped. Textbook moment for extracting a check, and the chapter declares "not much to refactor" instead — the triplication is the lesson it walks past.
“You can redirect the user when ErrNotFound is encountered, but display an error message when ErrWordDoesNotExist”
Read in context →The justification undercuts itself — both errors here mean the same absence, discovered by the same lookup. The real reason for a distinct error is the caller's intent (update vs read), not new information.
“it makes the errors more reusable and immutable”
Read in context →The actual win is narrower: a const can't be reassigned by another package. But a typed string error also loses identity — two distinct errors with the same text become == equal, which quietly weakens the sentinel.
A reader’s takeAI reading companion
The chapter's real subject: making absence explicit, not maps
A well-paced TDD walkthrough whose real subject isn't maps at all — it's the discipline of making absence explicit. The map material is thin but correctly chosen: comma-ok, delete, nil-write panic, and the Cheney clarification that a map is a copied pointer, which is the one genuinely load-bearing fact. The chapter's strength is its rhythm; each failing message is shown, so you learn to read the compiler as a collaborator rather than an obstacle. Its weakness is that the CRUD expansion teaches by accumulation. Add, Update, and Delete end up as the same nine lines with one clause moved, and the author says "not much to refactor" at exactly the point where a reader should feel the itch. More pointed: the tests are upgraded to errors.Is while the implementations keep comparing with ==, so the book models modern error handling in one file and pre-wrapping style in the other, without noting the gap. The const-error finale is presented as pure gain when it's a trade — immutable and reusable, yes, but string-valued errors collide on equality, which is an odd note to end a chapter about precise error identity on. Read it for the loop, not the conclusions.
Read Go fundamentals with these cards beside each passage.
Open in FeedLitraGo fundamentals: Dependency Injection
How does mocking extend this once no standard-library Writer fits?
How Go fundamentals unfoldsAI-assisted
The chapter opens by framing dependency injection as a widely misunderstood topic and setting out four claims up front: you need no framework, your design doesn't get more complicated, testing becomes easier, and your functions become more general-purpose. It then reaches back to the book's very first exercise — a Greet function that prints a hello message — and poses the problem that drives everything after: fmt.Printf writes to stdout, which the testing framework can't easily capture. The proposed fix is to "inject" the printing dependency, i.e. pass it in, and to accept an interface rather than a concrete type so the destination of the output is no longer hard-wired.
The development section is a short exercise in reading the standard library rather than inventing an abstraction. The author walks down through fmt.Printf to fmt.Fprintf to discover that Printf is merely Fprintf with os.Stdout supplied, and that Fprintf's first parameter is an io.Writer — an interface with a single Write method. The inference is that os.Stdout satisfies that interface, and the remark is made that io.Writer recurs constantly in Go as the general-purpose way of saying "put this data somewhere." From there the usual test-first cycle runs: a test writes into a bytes.Buffer (which also satisfies Write) and compares its contents against the expected greeting; the test fails to compile; a minimal Greet taking a *bytes.Buffer is written so the compiler is satisfied but the test still fails, with the greeting visibly going to stdout instead of the buffer; swapping Printf for Fprintf on the writer makes it pass.
The turn comes in the refactor. Passing *bytes.Buffer was merely what the compiler demanded, and the chapter exposes how useless that is by trying to wire Greet into a real program with os.Stdout — which won't type-check, since os.Stdout is an *os.File. Widening the parameter to io.Writer, the interface both types implement, makes one function serve both the test and the application. The chapter then pushes the point further with a second context: a small HTTP server where Greet is called inside a handler, writing to the http.ResponseWriter that likewise satisfies io.Writer — details of HTTP deferred to a later chapter, the point being reuse.
It closes by naming the three payoffs the refactor bought. Testability, with the broader diagnostic that hard-to-test functions usually signal hard-wired dependencies or global state — a shared database pool being the stock example, which DI pushes you to inject behind an interface and mock. Separation of concerns, decoupling where data goes from how it's produced, offered as the tool to reach for when a function is doing too many jobs at once. And reusability across contexts, of which the test is simply the first. Two closing asides round it off: mocking is deferred to a later chapter and defended as not evil, merely unnecessary here because the standard library already supplied a usable writer; and the standard library deserves study, since familiarity with interfaces like io.Writer is what makes this kind of reuse available at all. The chapter credits The Go Programming Language as the source of the example.
Key claims, with the lines that support them
A function that prints directly to standard output is hard to test, so the place it writes to should be handed in from outside.
The author traces the standard library: the familiar print function is only a thin wrapper that forwards to a variant taking a writer, with standard output supplied as that writer. That reveals the writer interface as the seam to hook into, and the bytes buffer type qualifies as such a writer because it has the required Write method — so a test can capture the greeting and inspect it.
“But how can we test this? Calling fmt.Printf prints to stdout, which is pretty hard for us to capture using the testing framework.”
Dependency InjectionRead in context →
The injected parameter should be the general writer interface, not a concrete buffer type.
This follows directly from the previous step: the compiler first pushed the author toward a buffer pointer, which passed the test but then refused to compile when the real program tried to pass standard output, a different concrete type. Widening the parameter to the shared interface — which both the buffer and standard output satisfy — made one function work in the test and in the application. The author notes the narrow concrete type is a legitimate waypoint while following the compiler, just not the destination.
“If we change our code to use the more general purpose interface we can now use it in both tests and in our application.”
Dependency Injection › RefactorRead in context →
Once the dependency is an interface, the same function can be reused in contexts it was never written for.
Without changing a line of the greeting function, the author drops it into a web request handler: the response writer given to handlers also satisfies the writer interface, so visiting the running server shows the same greeting delivered over HTTP instead of to a terminal.
“If we change our code to use the more general purpose interface we can now use it in both tests and in our application.”
Dependency Injection › RefactorRead in context →
2 more claims
If a function is hard to test, the cause is usually dependencies wired into it or reliance on global state.
The author's illustration is a service layer leaning on a single global database connection pool: tests of that layer are both difficult to set up and slow to run, until the database is injected through an interface and replaced in tests by something controllable. The author presents this as a typical pattern rather than a measured result.
“Test our code If you can't test a function easily, it's usually because of dependencies hard-wired into a function or global state.”
Dependency Injection › Wrapping upRead in context →
Familiarity with the standard library is what makes this kind of decoupling cheap.
The author never defines a new abstraction: the general-purpose writer interface already existed, the buffer type already implemented it for tests, and standard output and the HTTP response writer implemented it for production. Knowing those pieces is the only reason a single extra parameter was enough to make the function both testable and reusable.
“But how can we test this? Calling fmt.Printf prints to stdout, which is pretty hard for us to capture using the testing framework.”
Dependency InjectionRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Passing in the destination makes output checkable
A function that calls Printf sends its text to stdout, which a test cannot read back. If the destination is handed in as a parameter instead, the test supplies a buffer, the function writes there, and the test simply reads the buffer's contents and compares them to the expected string.
Mechanism 2
One method is enough to fit many destinations
The standard write interface demands only a Write method, so any type that has one qualifies. Declaring the parameter as that interface rather than a buffer pointer means the test's in-memory buffer, the program's standard output, and a web server's response writer are all accepted by the very same function.
Mechanism 3
Test friction points at wired-in dependencies
When a function reaches out to something fixed inside it — standard output, a shared database pool — tests cannot substitute anything, so they are awkward and slow. Moving that dependency to a parameter behind an interface splits producing the data from deciding where it goes, and each half can then be exercised on its own.
Common misreadings
Dependency injection means adopting an injection framework or container and letting your design get more elaborate.
Here it amounts to one extra function parameter typed as an existing standard-library interface — no library, no configuration, and the function body stays as short as before. The author opens the chapter by stating plainly that no framework is needed and that the technique does not complicate the design.
“What we need to do is to be able to inject \(which is just a fancy word for pass in\) the dependency of printing.”
Dependency InjectionRead in context →
If you inject dependencies you are committed to writing mocks, and mocks are a bad smell to be avoided.
Mocking is just substituting a controllable stand-in for the injected real thing so a test can inspect it, and the author treats it as a legitimate tool rather than a sin. It is also not always required: in this chapter the standard library's in-memory buffer already served as the substitute writer, so nothing had to be faked by hand.
“You use mocking to replace real things you inject with a pretend version that you can control and inspect in your tests.”
Dependency Injection › What about mocking? I hear you need that for DI and also it's evilRead in context →
Key concepts in Go fundamentals
- Dependency Injection
- Passing a function's dependency in from outside instead of hard-wiring it inside.
- Where it applies: No framework required; it is just passing a value, usually behind an interface.
- io.Writer
- A one-method standard interface meaning "put this data somewhere".
- Where it applies: Only covers writing bytes; implementers include os.Stdout, bytes.Buffer, http.ResponseWriter.
- Mocking
- Replacing an injected real dependency with a controllable pretend version in tests.
- Where it applies: Not always needed; here the standard library's bytes.Buffer sufficed.
0 more concepts
Reader’s notes on Go fundamentalsAI reading companion
“What we need to do is to be able to inject \(which is just a fancy word for pass in\) the dependency of printing.”
Read in context →The whole chapter's thesis is this parenthesis. Strip the enterprise vocabulary and DI is function arguments. That's the deflation the title promised.
“If you look at the source code of fmt.Printf you can see a way for us to hook in”
Read in context →The real pedagogy: the answer wasn't invented, it was read off the stdlib. Testability was already designed into Printf by whoever split it from Fprintf.
“func Greet(writer *bytes.Buffer, name string) { fmt.Printf("Hello, %s", name”
Read in context →A deliberately absurd intermediate step — accept a writer and then ignore it. Obeying the compiler literally before obeying the test is the whole TDD discipline in one gag.
5 more notes
“Hello, Chris di_test.go:16: got '' want 'Hello, Chris'”
Read in context →Lovely detail: the greeting appears in the test output yet the assertion sees nothing. The failure message physically shows you where the data went.
“This is technically correct but not very useful.”
Read in context →The compiler is a good proofreader and a bad architect. It will happily nail you to a concrete type; widening to io.Writer is a judgment it can't make for you.
“You can probably guess that http.ResponseWriter also implements io.Writer”
Read in context →Nobody at net/http registered anything with bytes or os. Structural typing means the reuse is retroactive — the interface collects implementers it never knew about.
“If you have a global database connection pool for instance”
Read in context →The honest escalation. Swapping a Writer is free; injecting a DB means owning transactions, lifetimes, and a mock that drifts from reality. The chapter wins easily because it chose the easiest dependency.
“In our case though, the standard library had something ready for us to use”
Read in context →Quietly the strongest argument against mocking frameworks: a real bytes.Buffer is both the fake and the assertion. You test behaviour, not a recording of calls.
A reader’s takeAI reading companion
Read how Printf solves it, that's the lesson here
A short chapter doing two jobs at once, and the second one is the better one. On the surface it teaches dependency injection by the smallest possible example: a function that prints becomes a function that writes, and suddenly it is testable, embeddable in a CLI, and droppable into an HTTP handler without a line of change. The argument is airtight because the dependency chosen is the friendliest one imaginable — io.Writer is a single method with no lifecycle, no error semantics worth arguing about, and a stdlib fake (bytes.Buffer) that is not a fake at all but a real implementation. The author is upfront enough to gesture at the hard case, the global DB pool, but the chapter never has to pay that bill: no setup/teardown, no mock drift, no question of how many seams is too many.
Read sceptically, this is DI on easy mode, and a reader who generalizes from it will be surprised the first time injection means threading a dependency through six layers of constructors. The deeper lesson, and the one I'd keep, is methodological: the solution was found by reading fmt's source. Printf is a thin wrapper over Fprintf because someone long ago made exactly the refactor this chapter walks through, and the abstraction has been sitting there waiting. Combine that with Go's implicit interfaces — nothing in net/http ever declared allegiance to io.Writer, yet ResponseWriter satisfies it — and reuse becomes something you discover rather than plan.
Continue reading
The chapter's quiet claim is that in Go, design taste is largely stdlib familiarity. Also worth noting how much of the teaching lives in the failure output: the moment where 'Hello, Chris' prints to the terminal while the assertion reports an empty string is a more vivid definition of an untestable dependency than any paragraph could be.
Read Go fundamentals with these cards beside each passage.
Open in FeedLitraGo fundamentals: Mocking
How would you decide whether sleep order is behaviour or implementation detail?
How Go fundamentals unfoldsAI-assisted
Opening: a trivial task, deliberately sliced. The chapter sets a toy requirement — count down 3, 2, 1, then "Go!", one line at a time with a pause between — and immediately uses it to argue for iterative discipline rather than for cleverness. The author divides even this small job into three slices (print 3; print the whole sequence; add the pause) on the grounds that developers fall into rabbit holes when they write code that will "theoretically work after some hacking." The first slice is driven by a test that writes into a bytes.Buffer, which lets the chapter reprise dependency injection from the preceding section: the function takes an io.Writer, so production code can hand it os.Stdout while tests capture output. The red-green-refactor cycle is walked through at compiler-error granularity — undefined function, wrong arity, wrong assertion — and only after the test passes does the parameter get generalized from *bytes.Buffer to io.Writer. The slice is then wired into main so there is working end-to-end software, a habit the author recommends for "any project."
Development: the loop, then the problem the loop creates. The second slice is cheap because the plumbing already exists: the test's expected output becomes a backtick multi-line string, a backwards for loop with Fprintln plus a final Fprint makes it pass, and the refactor step only lifts "Go!" and 3 into named constants. Then time.Sleep is dropped in, the program behaves exactly as specified — and this is the chapter's pivot. The author lists what the working code costs: tests now take three seconds (slow tests "ruin developer productivity," and the cost compounds with every new test case), and the pause itself is untested. The fix is the chapter's title move: define sleeping as a Sleeper interface with a single Sleep() method, inject it, use a real implementation in main and a spy — a test double that records how it was used — in tests. A design decision is flagged explicitly: Countdown does not own the sleep duration, leaving that to the caller.
The turn within the turn: counting calls isn't enough. Having made the test assert three calls to Sleep, the author refuses to stop there, because the ordering property — sleep interleaved between prints — is still unverified. The proof is a deliberate sabotage: split the function into one loop that sleeps three times and another that prints, and the test still passes. The advice generalizes ("if you're not confident that your tests are giving you sufficient confidence, just break it," after committing). The repair is a single spy, SpyCountdownOperations, that satisfies both io.Writer and Sleeper and appends a "write" or "sleep" label to one slice, so a reflect.DeepEqual against the expected sequence pins the interleaving. The suite is then reorganized into two sub-tests — one for output, one for ordering — and the now-redundant first spy is deleted. A final extension makes the sleeper configurable: ConfigurableSleeper holds a duration and a function with time.Sleep's signature, so tests inject a SpyTime recording the duration requested while main injects the real thing; DefaultSleeper becomes dead code and goes.
Closing: the caveats, and mocking's bad reputation. The last third drops the exercise and argues with the reader's likely objection that mocking is evil. The author's position is that the technique is blameless but the feedback it gives is real: complicated mocks or a pile of them usually indicate a unit doing too much, dependencies carved too finely, or tests entangled with implementation rather than behavior — "well-designed code is easy to test," so painful mocks are TDD reporting a design problem, not causing one. Concrete heuristics follow: refactoring should ideally require no test changes, so ask whether you're testing behavior or internals; avoid testing private functions (Sandi Metz's "less stable" code); treat more than three mocks in one test as a red flag; use spies sparingly, since recording an algorithm's internal steps couples the test to them. Mocking frameworks get a measured verdict — hand-rolling teaches more and exercises interface implementation, while auto-generated doubles earn their place in teams by enforcing consistency, provided the tool generates against an interface and doesn't dictate how tests are written. The wrap-up restates the two threads (thin vertical slices, quoting Fowler on iterative development; and mocking as the only practical way to test failure, timing and particular system states without slow, fragile real dependencies), notes that spies are just one species of test double alongside stubs and mocks proper, and ends with a Go 1.23 bonus: a countDownFrom iterator returning iter.Seq[int] that yields descending numbers, letting the loop in Countdown read as a range over an expressive custom sequence.
Key claims, with the lines that support them
Build software in thin end-to-end slices that already work and are already tested, rather than in one big push.
The author deliberately ships a version that prints only the number 3, wires it into a runnable program, and only then extends it to print 3, 2, 1 and the final word, and later adds the pauses. Each stage is working software with tests, so the plumbing is proven before any logic grows on top, which is what keeps the later refactors cheap.
“Take a thin slice of functionality and make it work end-to-end, backed by tests.”
Mocking › RefactorRead in context →
A real pause in production code must be pulled out behind an injectable dependency.
With a genuine one-second wait inside the loop, the test suite spent three seconds doing nothing, and the pause was never actually verified. Replacing it with an injected sleeper and a recording stand-in makes the suite instant while finally making the waiting behaviour testable — the pause changes from an untested side effect into an asserted property.
“We have a dependency on Sleeping which we need to extract so we can then control it in our tests.”
MockingRead in context →
Checking that a dependency was called the right number of times says nothing about when it was called.
The author breaks the implementation on purpose: first sleep three times, then print all three numbers. The count-based test still passes even though the output is no longer a dramatic countdown. That failure to fail is the argument for a second test that compares the recorded sequence of writes and sleeps, with the caveat that such order checks bind the test more tightly to the algorithm.
“Our latest change only asserts that it has slept 3 times, but those sleeps could occur out of sequence.”
Mocking › Still some problemsRead in context →
3 more claims
When mocking hurts, the message is about your design, not about test-driven development.
The author treats complicated or sprawling fakes as a diagnostic: it usually means the unit has too many dependencies and should be split, that its dependencies are too fine-grained and should be consolidated, or that the test is chasing implementation detail instead of behaviour. So the discomfort people cite as TDD's weakness is reframed as its strength — well-designed code is easy to test.
“What people see here is a weakness in TDD but it is actually a strength.”
Mocking › But isn't mocking evil?Read in context →
Aim tests at observable behaviour so that refactoring does not drag a pile of test edits with it.
Since refactoring means the code changes while behaviour stays the same, the author argues you should in principle be able to commit a refactor without touching tests; needing many test changes is the symptom of over-specified tests. The same logic rules out testing private functions, which are described as less stable supports for public behaviour, and it suggests more than three fakes in one test is a signal to rethink the design.
“What people see here is a weakness in TDD but it is actually a strength.”
Mocking › But isn't mocking evil?Read in context →
You can write your own test doubles by hand; a framework is optional, not a prerequisite.
The whole chapter mocks nothing more exotic than a struct with a counter or a slice, which the author uses to show that mocking needs no magic, while also giving practice at implementing interfaces. Generated doubles are still granted value on teams, where a tool enforces consistency — but only tools that generate against an interface and do not dictate how tests are written.
“What people see here is a weakness in TDD but it is actually a strength.”
Mocking › But isn't mocking evil?Read in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Hiding a dependency behind a method set
The pause is renamed as a one-method contract that the countdown function merely calls. Production passes a sleeper that really waits; the test passes a fake that only records, so the same code runs with no wall-clock delay and the pause itself becomes something a test can assert on.
Mechanism 2
One recorder for two collaborators
Because a single fake implements both the writer and the sleeper, every print and every pause lands in one shared list in the order it happened. Comparing that list against an expected sequence catches interleaving errors that a bare counter of sleep calls cannot see.
Mechanism 3
Mock count as a design readout
Each thing you must fake corresponds to a dependency the unit reaches for, so the effort of setting up a test scales with how coupled the unit is. When the fakes pile up or get convoluted, that difficulty is measuring the design: too many responsibilities, dependencies chopped too thin, or assertions aimed at internals.
Common misreadings
Mocking is a bad practice, so a disciplined developer should test against the real thing instead.
The author's position is that mocking is a neutral tool that can be misused, and that skipping it leaves important paths untested — you could not verify the pause between prints, nor easily test a failing service or a particular system state. Without doubles you end up spinning up databases and third-party services to check simple rules, producing slow and flaky tests.
“What people see here is a weakness in TDD but it is actually a strength.”
Mocking › But isn't mocking evil?Read in context →
Since spies caught the ordering bug, recording as much internal detail as possible makes tests stronger.
Spying is explicitly flagged as something to use with care: every internal detail you record ties the test to the current algorithm, so you should only assert on details you genuinely care about. Over-testing how the code works rather than what it does is what makes later refactoring expensive.
“Spies are a kind of mock which can record how a dependency is used.”
Mocking › Write the test firstRead in context →
Key concepts in Go fundamentals
- Spy
- A test double that records how a dependency was used, such as call counts or call order.
- Where it applies: Recording internals couples tests to implementation, so only spy on details you truly care about.
- Test Double
- Any stand-in object used in place of a production object during testing.
- Where it applies: Covers stubs, spies and mocks; this chapter only demonstrates spies.
- Refactoring
- Changing code while behaviour stays the same, which should ideally require no test changes.
- Where it applies: If refactoring forces many test edits, the tests were bound to implementation detail.
- Interface
- A named set of methods that lets a function accept a real or fake implementation interchangeably.
- Where it applies: Works only when the dependency can be expressed behaviourally; the caller decides the concrete type.
- Iterator
- A function shaped so a for-range loop can pull values from it, introduced in Go 1.23.
- Where it applies: Requires one of the accepted iterator function signatures; used here purely for expressiveness.
0 more concepts
Reader’s notes on Go fundamentalsAI reading companion
“I made a design decision that our Countdown function would not be responsible for how long the sleep is.”
Read in context →Worth pausing: this is the chapter's quietest and most consequential choice. A Sleep() with no duration is barely a time abstraction — it's a "pause here" marker. That's why the interface stays testable.
“just break it! (make sure you have committed your changes to source control first though)”
Read in context →Mutation testing by hand. The real lesson isn't the trick, it's the epistemics: a passing test proves nothing until you've seen it fail for the right reason.
“Our SpyCountdownOperations implements both io.Writer and Sleeper”
Read in context →Clever — one spy collapses two dependencies into a single ordered log. Also the moment the test stops testing behaviour and starts transcribing the algorithm, which the author warns against twenty paragraphs later.
6 more notes
“Without mocking important areas of your code will be untested.”
Read in context →The honest defence. But note the chapter never considers the alternative it just made unnecessary: inject a clock, assert elapsed virtual time. Order-of-calls is one answer, not the only one.
“you might wonder why all the hassle as the main program didn't change at all”
Read in context →Rare moment of the author naming the reader's boredom out loud. And then the payoff is... one line in main. The ceremony-to-value ratio here is the weakest stretch of the chapter.
“func (s *SpyTime) SetDurationSlept(duration time.Duration”
Read in context →A spy named like a setter. Naming leaks intent badly here: this records, it doesn't configure. Small thing, but it's the sort of blur that makes later readers distrust test code.
“well-designed code is easy to test”
Read in context →Stated as if it reverses cleanly. It doesn't — easy-to-test code can also be code shredded into interfaces purely to satisfy mocks. The chapter's own Sleeper flirts with that.
“Any tool that overly dictates how tests are written, or that use lots of 'magic', can get in the sea.”
Read in context →The voice finally drops the pedagogical register and you hear an actual opinionated engineer. Tonal relief — and more persuasive than the careful paragraph before it.
“we can make this code look more expressive by range-ing over our custom countdownFrom iterator”
Read in context →Honest question: is it more expressive, or just newer? for i := 3; i > 0; i-- needs no explanation; iter.Seq[int] with a yield-returns-bool protocol needs a paragraph of docs. The bonus section quietly undercuts the book's own minimalism.
A reader’s takeAI reading companion
Teaching spy testing while warning against its coupling
The chapter is strongest as a demonstration of iteration discipline — thin vertical slices, compiler errors used as a design conversation, the deliberate sabotage to validate a test — and weakest when it can't quite reconcile its two halves. It spends the first half teaching you to spy on call order, then the second half telling you spies couple tests to implementation and that more than three mocks is a red flag. Both are true; the chapter never admits they're in tension in its own example. The Sleeper interface survives scrutiny only because the author made it duration-free, which is a smart move presented almost in passing, while the ConfigurableSleeper section is the clearest case of TDD ritual outrunning its payoff — the author even notices and says so.
The Go 1.23 iterator coda reads as appended rather than argued: it trades a self-evident loop for a protocol that needs documentation, in a book whose whole method is smallest-useful-step. What I'd keep: break your test on purpose, and ask whether you're testing what the code does or how it does it. What I'd push back on: the chapter models the exact spy-heavy habit it later cautions against, and leaves the reader to notice.
Read Go fundamentals with these cards beside each passage.
Open in FeedLitraGo fundamentals: Concurrency
What happens on a go.mod older than 1.22, and why does passing url in fix it?
How Go fundamentals unfoldsAI-assisted
The chapter opens with a handoff rather than a blank page: a colleague's CheckWebsites function, which loops over a slice of URLs and fills a map with each URL's boolean status, plus the test they already wrote using a mock checker. The authors make a point of praising the existing design — dependency injection means the function can be tested without real HTTP calls — so the reader understands that this code already "works" and is already "right." The only complaint is speed, and the chapter's first move is to turn that complaint into something measurable: a benchmark that feeds in a hundred URLs and a deliberately slow stub checker that sleeps twenty milliseconds per call. Run under go test -bench=., it reports roughly 2.25 seconds per operation. The problem now has a number attached to it.
From there the chapter develops the concept before the code. Concurrency is defined plainly as "having more than one thing in progress," illustrated with the tea-making analogy: you don't stand and stare at the kettle, you fetch milk and mug while it boils. That sets up the distinction between blocking calls, which make you wait, and goroutines, which the go keyword launches as a separate process — likened to a second reader moving through the code while the first carries on down the page. The naive application follows: wrap the loop body in an anonymous function and prefix it with go. The chapter also explains why anonymous functions suit this purpose, since they can be invoked where they're declared and retain access to their surrounding lexical scope.
The turn is a sequence of failures, each diagnosed rather than merely patched. First the test returns an empty map: CheckWebsites finishes and returns before any goroutine has written its result. An aside warns that the reader may instead see a panic, and reframes that unpredictability as the reason tests matter here. The obvious fix — time.Sleep(2 * time.Second) before returning — makes the test pass if you're lucky, but if you're not, the program dies with fatal error: concurrent map writes. This is named as a data race: two or more goroutines touching the same memory with at least one write, and Go maps are not safe for concurrent writes. The chapter then teaches the tool for finding such bugs, go test -race, and walks through the race detector's output, showing how the warning identifies the two conflicting writes at the same address and the lines where the offending goroutines were created. The recurring advice is that the output looks frightening but is readable if you slow down.
The resolution is channels. A small anonymous-field result struct pairs a URL with its boolean, and each goroutine sends one onto a resultChannel instead of writing the map directly; a second loop, running once per URL, receives from the channel and performs the map writes sequentially. The <- operator is explained in both directions — channel on the left to send, channel on the right to receive. The lesson drawn is a division of labour: the slow part runs concurrently, the part that cannot safely happen at once stays linear, and channels carry the communication between them. A note on loop-variable capture clarifies that since Go 1.22 the loop variable is fresh each iteration so capturing url is safe, but that an older go directive in go.mod keeps the old shared-variable semantics regardless of toolchain version, in which case you pass url into the goroutine as a parameter. The benchmark now reports about 0.023 seconds — roughly a hundredfold improvement.
The closing section frames the whole exercise as one long refactoring: inputs and outputs never changed, so the existing test and the new benchmark together gave confidence that the code still worked while proving it had got faster. The takeaways are listed as goroutines, anonymous functions, channels, and the race detector. A final coda invokes the slogan "make it work, make it right, make it fast" — noted as often misattributed to Kent Beck — and stresses that this chapter only got to skip the first two steps because the inherited code already satisfied them. The chapter ends with Knuth's line about premature optimization being the root of all evil, which retroactively justifies the whole structure: measure first, optimize last.
Key claims, with the lines that support them
The speed comes from overlapping the waiting, not from doing less work.
The author compares it to making tea: you fill the kettle and then fetch the milk, mug and teabag while it boils, instead of staring at the kettle and only starting afterwards. Applied to the checker, the computer issues the next request while the previous one is still outstanding; against a stub that sleeps 20ms per url, 100 urls took about 2.25 seconds serially and roughly 0.023 seconds once overlapped.
“By sending the results into a channel, we can control the timing of each write into the results map, ensuring that it happens one at a time.”
Concurrency › ChannelsRead in context →
Launching goroutines on its own breaks a working function, because nobody waits for them.
After wrapping only the loop body in a goroutine, the colleague's existing test fails with an empty map — proof that the results were never recorded before the return. The author notes the outcome is not even stable: some runs fail this way, others panic.
“None of the goroutines that our for loop started had enough time to add their result to the results map; the CheckWebsites function is too fast for them, and it returns the still empty map.”
Concurrency › ... and we're back.Read in context →
Waiting with a sleep is not a fix; it swaps one unreliable behaviour for another.
This is set up as the obvious next move after the empty-map failure, then knocked down: with a two-second sleep the test sometimes passes and sometimes dies with a fatal error about concurrent map writes, and the crash shows up more often when run alongside the benchmark. The sleep lets the goroutines finish but does nothing about two of them writing the same map at the same instant.
“Go maps are not safe for concurrent writes, so the runtime throws a fatal error to prevent memory corruption.”
Concurrency › ... and we're back.Read in context →
2 more claims
Concurrency bugs are debuggable rather than mysterious, if you use the tooling and read what it prints.
Because the failure depends on timing, the author reaches for the built-in race detector via go test -race, whose report names the shared memory address, both conflicting writes, and the exact source lines where the writes happen and where the goroutines were started. The author's point is that the output looks frightening but contains everything needed; patience, not guesswork, resolves it.
“Go maps are not safe for concurrent writes, so the runtime throws a fatal error to prevent memory corruption.”
Concurrency › ... and we're back.Read in context →
Make it fast comes last, after the code works and has been cleaned up.
The whole chapter is framed as one long refactor: inputs and outputs never changed, so the colleague's pre-existing test could certify that behaviour survived while the new benchmark certified the gain. Without that safety net the rewrite would have been a blind gamble — which is why the author pairs the agile slogan with Knuth's warning about premature optimization.
“We should never try to 'make it fast' before the other two steps have been performed because”
Concurrency › Make it fastRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Why the goroutine version returns nothing
Putting go in front of the loop body means the caller never waits for that work. The function finishes its loop and returns the map in the time it takes to launch the goroutines, so the writes land (if at all) after the caller already handed back an empty map.
Mechanism 2
Why sleeping only hides the bug
A sleep buys time but imposes no order: every goroutine still writes straight into one shared map whenever it happens to finish. Go maps are not safe for simultaneous writes, so when two writes collide the runtime aborts the program rather than silently corrupting memory.
Mechanism 3
How a channel serializes the writes
Each goroutine stops touching the map and instead sends a url/result pair into a channel. The parent then receives once per url, so the slow checks still overlap in parallel while the one thing that cannot be done simultaneously — updating the map — happens one value at a time in a single process.
Common misreadings
The concurrent version passed when I ran it, so there is no race in it.
Races depend on timing, so a green run proves only that the collision did not happen that time — the sleep-based version passed on some runs and aborted on others. Confidence has to come from removing the shared write (via the channel) and from checking with the race detector, not from a lucky run.
“This is a data race, a bug that occurs when two or more goroutines access the same memory location concurrently, and at least one of those accesses is a write.”
Concurrency › ... and we're back.Read in context →
Capturing the loop variable inside a goroutine is fine now, so url never needs to be passed in explicitly.
That safety depends on the go version declared in go.mod, not on how new your toolchain is: from 1.22 onwards each iteration gets a fresh url, but with an older declared version all iterations share one variable and the goroutines will likely all see its final value. On an old go.mod, pass the url into the anonymous function as a parameter.
“Secondly they maintain access to the lexical scope in which they are defined - all the variables that are available at the point when you declare the anonymous function are also available in the body of the function.”
Concurrency › Write enough code to make it passRead in context →
Key concepts in Go fundamentals
- Goroutine
- A separate process started by putting
goin front of a function call, so the caller does not block waiting for it. - Where it applies: Starting one gives no guarantee it finishes before the caller returns.
- Anonymous function
- A function literal with no name that can run where it is declared and keeps access to surrounding variables.
- Where it applies: Its capture of loop variables depends on the go directive version in go.mod.
- Channel
- A Go data structure that sends and receives values, letting processes communicate and take turns.
- Where it applies: Here it serializes map writes; the receive loop must run once per expected value.
- Data race
- Two or more goroutines touching the same memory at once with at least one writing.
- Where it applies: Timing-dependent, so a passing run does not prove absence.
- Race detector
- A built-in tool, enabled with the -race flag, that reports concurrent conflicting accesses and the lines involved.
- Where it applies: Reports races it observes during the run, not all possible ones.
- Benchmark
- A timing test used here to measure the function before and after the change.
- Where it applies: Uses a deliberately slow stub, so it measures coordination, not real network behaviour.
0 more concepts
Reader’s notes on Go fundamentalsAI reading companion
“Using dependency injection has allowed them to test the function without making real HTTP calls”
Read in context →Note the setup rewards good prior design: the colleague's DI is what makes a benchmark honest. Optimization is cheap only because testability was paid for earlier.
“I put the kettle on and then, while I was waiting for it to boil”
Read in context →The tea analogy covers concurrency as overlap but quietly skips the hard part — two hands reaching for the same mug. The data race is exactly what the metaphor can't show.
“Don't worry if you got that, just keep running the test until you do get the result above.”
Read in context →A startling instruction in a TDD book: run until you get the failure you want. It's honest about nondeterminism, but it also teaches that a passing concurrent test proves almost nothing.
6 more notes
“Two seconds ought to do it, right?”
Read in context →The rhetorical wink is the best pedagogy in the chapter — the wrong fix is shown working before it's shown broken, which is exactly how sleep-based 'fixes' survive in real codebases.
“all we need to do is take a breath and read the stacktrace”
Read in context →Underrated lesson, worth more than the channel trick: the terminal already told you everything; the skill being taught is not panicking at wall-of-text output.
“Since Go 1.22, this is safe: the language spec was changed”
Read in context →A patch over a famous footgun, and the warning about a new toolchain with an old go.mod is the real trap — the semantics follow the declared version, not the compiler you installed.
“while making sure that the part that cannot happen simultaneously still happens linearly”
Read in context →This is the actual thesis, buried mid-paragraph. Concurrency design is mostly deciding which slice must stay serial — channels are just the funnel.
“about one hundred times as fast as original function”
Read in context →Suspiciously clean, because the stub sleeps and does nothing else. Real HTTP has connection limits and DNS; 100 goroutines against 100 real hosts won't scale this linearly.
“Premature optimization is the root of all evil -- Donald Knuth”
Read in context →Quoted as a closer right after a chapter that was entirely optimization — and the Knuth line, in full, actually argues the opposite of how it's usually deployed.
A reader’s takeAI reading companion
Speed-up story teaching how to fail safely instead
A deceptively gentle chapter: it reads as a speed-up story but is really a lesson in how to fail safely. Its strongest move is pedagogical dishonesty-in-good-faith — showing the naive goroutine loop, then the sleep hack, letting each pass once before breaking, so the reader feels why the correct version exists rather than being told. The weakest seam is that the benchmark's hundredfold win is an artifact of a sleeping stub; the chapter never flags that real networks impose their own serialization, nor does it mention sync.WaitGroup, buffered channels, or any bound on goroutine count — fine for 100 URLs, a hazard at 100,000. The Go 1.22 loop-variable note is the most practically dangerous paragraph and the most quietly delivered: a correctness property that depends on a line in go.mod rather than on your installed compiler. And the Knuth closer sits oddly on a chapter devoted to making something fast — invoked as permission rather than examined. What stays with me is 'the part that cannot happen simultaneously still happens linearly': the whole discipline compressed into a clause the author doesn't bother to emphasize.
Read Go fundamentals with these cards beside each passage.
Open in FeedLitraGo fundamentals: Select
What happens to the losing goroutine after select returns?
How Go fundamentals unfoldsAI-assisted
The opening: a concrete brief, deliberately solved badly. The chapter begins with a specification rather than a concept — write a function that takes two URLs, hits both with an HTTP GET, returns whichever responds first, and errors out if neither answers inside ten seconds. It lists the tools up front (net/http, net/http/httptest, goroutines, select), then immediately writes a test that the author openly calls imperfect: it races two real, live websites and asserts the faster one wins. This naive test drives the usual TDD cycle — a stub Racer that returns an empty string, a failing assertion, then an implementation that does the obvious sequential thing. It times one request with time.Now()/time.Since, then times the other, and compares the two durations. The pedagogical point is that it's fine to start with something crude; the code's flaws become the chapter's agenda.
First development: the test is the problem, not the code. The chapter pauses on a worry — the test may or may not pass, because it depends on the actual internet. Echoing the earlier mocking and dependency-injection chapters, it names the three sins of testing against external services: slowness, flakiness, and the impossibility of exercising edge cases. The fix is httptest.NewServer, which spins up a real HTTP server on a free port backed by an anonymous http.HandlerFunc. One fake server sleeps briefly before replying; the other replies at once, so "fast" and "slow" become properties the test controls rather than facts about Facebook's infrastructure. A refactoring step then cleans both sides: production code gains a measureResponseTime helper that removes the duplicated timing block, and the test gains a makeDelayedServer(delay) factory. This is also where defer is introduced — a way to schedule server cleanup at function exit while writing the instruction next to the creation it mirrors, for the benefit of later readers.
The turn: stop timing, start racing. With a tidy but still sequential solution in hand, the chapter asks the question it has been setting up — why measure the two sites one after another when Go is built for concurrency, and why measure precise durations at all when the only fact we need is which came back first? That reframing produces the chapter's centrepiece. A ping function launches a goroutine that performs the GET and then closes a channel, and Racer wraps two ping calls in a select, returning the URL whose channel fires first. Several small lessons hang off this: chan struct{} is used because the signal carries no payload and struct{} allocates nothing; closing a channel is itself a perfectly good "done" signal; channels must be created with make, since a var-declared channel is nil and sending to it blocks forever; and select generalises the blocking receive from the concurrency chapter to several channels at once. The result is both shorter and more honest about intent than the version it replaces.
Second development: the timeout, and then the timeout's cost. Only now does the chapter return to the deferred requirement. A second subtest expects an error when both servers take longer than ten seconds, which forces Racer to grow a second return value; the minimal version compiles but fails after eleven real seconds. The fix is a third select case fed by time.After, presented as the standard guard against code that could otherwise block forever. But passing the test exposes a new flaw — the suite now burns ten seconds to verify a few lines of logic. Making the timeout a parameter breaks the happy-path test's call signature, and the chapter treats that friction as feedback ("listen to them") rather than an annoyance to paper over: the happy-path test doesn't care about timeouts, so it shouldn't have to mention them. The resolution is to split the API into Racer, which hard-codes the ten-second default the requirements specified, and ConfigurableRacer, which takes a timeout. Real users and the happy-path test call the former; the sad-path test calls the latter with millisecond-scale values, and a t.Fatalf error check is added to the happy path for completeness.
The close. The summary is brief and maps onto the two tools the chapter earned rather than merely described: select for waiting on multiple channels, usually with a time.After case so nothing can hang indefinitely, and httptest for reliable, controllable test servers that reuse the same net/http interfaces you already know. The arc, in short, runs from a slow sequential measurement tested against the real internet to a concurrent race tested against fakes in milliseconds — with each refactoring prompted by a specific discomfort in the previous step.
Key claims, with the lines that support them
Logic that talks to HTTP should be tested against servers you control, not against real websites.
The author's first version hits two real sites, and he points out it may or may not pass, since slow, flaky external services also make edge cases untestable. Replacing them with two local test servers — one sleeping 20ms, one answering instantly — fixes the winner in advance, and the author invites readers to change the sleeps to deliberately break the test, showing the outcome is now under their control.
“The problem is we're reaching out to real websites to test our own logic.”
Select › ProblemsRead in context →
Test servers are real resources, so cleanup belongs right where they are created.
Building on the move to fake servers, the author notes each one keeps listening on a port until closed, then prefixes the close calls with defer so they run when the test function ends while still sitting next to the setup code — readable for whoever comes later, and reliable without manual bookkeeping at every exit.
“The problem is we're reaching out to real websites to test our own logic.”
Select › ProblemsRead in context →
Running the two requests concurrently and picking the first answer is not only faster but simpler code than timing them one after another.
The sequential version records a start time, performs each GET, measures elapsed durations and compares them — duplication the author has to factor out into a helper. The concurrent version instead gives each URL a channel closed when its request finishes and lets select return whichever fires first; no clocks, no subtraction, and the intent reads directly off the page.
“After these changes, the intent behind our code is very clear and the implementation is actually simpler.”
Select › selectRead in context →
1 more claim
A test that takes ten seconds is design feedback, not just an annoyance.
Rather than accept the wait, the author makes the timeout a parameter — but then listens to the tests again and notices the happy path has no business specifying one. The result is a plain Racer that supplies a ten-second default for callers plus a ConfigurableRacer the slow-path test calls with twenty milliseconds, so both the users and the tests get the interface they actually need.
“What we can do is make the timeout configurable. So in our test, we can have a very short timeout and then when the code is used in the real world it can be set to 10 seconds.”
Select › Slow testsRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Closing an empty channel as a done signal
Each ping call starts a goroutine that performs the GET and then closes a channel carrying no data. Because a receive on a closed channel returns immediately, the close itself is the message, and an empty struct channel needs no allocation since nothing is ever sent through it.
Mechanism 2
Why a timeout branch stops endless waiting
A select statement blocks until one of its channels is ready, so if neither request ever completes the function would hang forever. Adding a case that reads from a timer channel guarantees some branch eventually fires, turning an indefinite wait into a returned error.
Mechanism 3
How fake servers make a race deterministic
A test server is a real HTTP server bound to a free port, but its handler is code you write, so inserting a sleep in one handler fixes which side is slower. The outcome then depends only on the racing logic rather than on network conditions, which is why the test both passes reliably and finishes quickly.
Common misreadings
Declaring a channel with var is enough to start using it; make is just a stylistic habit.
A channel's zero value is nil, and sending to a nil channel blocks forever, so the author stresses that channels must be created with make before use — unlike a string or int, whose zero values are usable.
“Why struct{} and not another type like a bool? Well, a chan struct{} is the smallest data type available from a memory perspective so we get no allocation versus a bool.”
Select › pingRead in context →
The finished Racer works by measuring how long each site takes and comparing the two times.
That was only the first, discarded implementation. The final version never times anything: it fires both requests concurrently and simply reacts to whichever channel signals first, because the requirement is to know the winner, not the response times.
“After these changes, the intent behind our code is very clear and the implementation is actually simpler.”
Select › selectRead in context →
Key concepts in Go fundamentals
- select
- A Go statement that waits on several channels and runs the branch of whichever one is ready first.
- Where it applies: Only useful when there are multiple channels to wait on; a case may block forever unless a time.After case is added.
- httptest
- Standard library package that spins up a real but controllable HTTP server for tests.
- Where it applies: Replaces external services in tests; still a real server, so it must be closed to free its port.
- defer
- Prefixing a call with defer runs it when the enclosing function ends, keeping cleanup next to setup.
- Where it applies: Runs at function exit, not block exit; meant for resource cleanup such as closing servers or files.
- time.After
- Returns a channel that receives a signal after a given duration, usable as a timeout case in select.
- Where it applies: Guards against blocking forever; the duration should be injectable so tests need not wait the production timeout.
- chan struct{}
- A signalling channel carrying no data; closing it is the message.
- Where it applies: Appropriate when only completion matters, not a value; channels must be created with make or sends block forever.
0 more concepts
Reader’s notes on Go fundamentalsAI reading companion
“We know this isn't perfect and has problems, but it's a start.”
Read in context →The real lesson of the chapter smuggled in early: permission to write a bad test. Most Go tutorials would hide the embarrassing first draft.
“The problem is we're reaching out to real websites to test our own logic.”
Read in context →Note the diagnosis is ownership, not speed — the test crosses a boundary it has no business crossing. Flakiness is just the symptom.
“a chan struct{} is the smallest data type available from a memory perspective”
Read in context →A micro-optimization dressed as idiom. The honest reason is semantic: struct{} says "there is no payload," which is exactly true here. Zero bytes is a bonus, not the argument.
4 more notes
“For channels the zero value is nil and if you try and send to it with <- it will block forever”
Read in context →Worth pressing: a nil channel in a select case simply never fires, which is a genuine idiom for disabling branches. The footgun and the tool are the same object.
“The first one to send a value "wins" and the code underneath the case is executed.”
Read in context →Quietly incomplete — if both are ready, select picks pseudorandomly. The racer survives this only because we don't care which of two tied winners we get.
“Before rushing in to add this default value to both our tests let's listen to them.”
Read in context →The best sentence in the chapter. Compilation failure reframed as feedback about API design rather than a chore to patch.
“case <-time.After(timeout):”
Read in context →The loser goroutine never gets cancelled — its http.Get keeps running and ping's channel is abandoned. Fine here, but this is exactly where context.Context would enter in real code.
A reader’s takeAI reading companion
Showing what fails plainly teaches better than arriving at answers
A chapter that teaches select almost incidentally; its real subject is letting tests apply pressure to a design. The arc — real websites, then httptest, then sequential measurement, then concurrent select, then a configurable timeout — is a sequence of listening acts, each refactor triggered by a discomfort in the test rather than a feature request. Two things it declines to say: select's choice among simultaneously-ready cases is pseudorandom, and the losing goroutine in the timeout case is simply abandoned, leaving an in-flight HTTP request and an orphaned channel. Both are harmless in a racer and both are exactly where a reader's next chapter has to go (context, cancellation, errgroup). I'd also push back on the struct{} justification: the memory argument is true but trivial, while the semantic argument — a closed channel of no-payload values is pure signal — is the one that should convince. What stays with me is the author's willingness to show the naive version and then dismantle it; the measureResponseTime refactor is deliberately a dead end, and saying so plainly is more instructive than arriving at select directly.
Read Go fundamentals with these cards beside each passage.
Open in FeedLitraGo fundamentals: Reflection
When is a method-based interface genuinely preferable to reflection here?
How Go fundamentals unfoldsAI-assisted
Opening: framing the problem and the warning. The chapter starts from a challenge posted on social media — write walk(x interface{}, fn func(string)) that calls fn for every string field inside an arbitrary struct, recursively — and immediately says the tool for the job is reflection, a program's ability to inspect its own types. Before touching code, the author spends a section on interface{} (any), explaining what you trade away by accepting it: callers lose type safety and documentation, and the function's author takes on the burden of figuring out at runtime what was handed over. The verdict is set up front: reflection is clumsy, harder to read, and slower, so reach for an ordinary interface and polymorphism first, and use reflection only when you genuinely must. This framing matters because this chapter then demonstrates the technique while advising restraint about it.
Development: a test-driven ratchet through the type system. The build is deliberately tiny at each step. First test: an anonymous struct with one Name field and a closure spy appending to a got slice, asserting only the call count — which is satisfied by the deliberately absurd step of calling fn with a hardcoded string about a football result. Tightening the assertion to the actual value forces the first real reflection: reflect.ValueOf, Field(0), String() — code the author openly calls unsafe and naive, justified because in the "red" phase you write the least code possible and then add tests to express your worries. The test is then refactored into a table test, which becomes the engine for the rest of the chapter: two string fields drives NumField and a loop; a non-string field (Age int, which leaks <int Value> into the output) drives a Kind() == reflect.String check; nested structs drive the recursive call on field.Interface(), and the pair of ifs collapses into a switch. A pointer input panics on NumField, which introduces Elem(), extracted into a small getValue helper — more code, but the right level of abstraction: get me a Value, then iterate and act by kind. Along the way the chapter also tidies the test itself, replacing nested anonymous struct literals with named Person and Profile types for readability.
The turn: a refactor that goes wrong, on purpose. Slices bring the same panic, patched first with an early-return special case the author calls "yucky," then reorganized so the switch dispatches on the kind of the value itself — struct, slice, string. Noticing that struct and slice iteration are conceptually identical, the author goes further and factors out numberOfValues plus a getField func(int) reflect.Value assigned from val.Field or val.Index. Arrays then come for free, bundled into the slice case with a comma — apparent vindication. Maps break it: by design you can't index a map positionally, only by key, so the elegant abstraction has to sit awkwardly beside a special-cased loop over MapKeys. The chapter treats this as the lesson rather than an embarrassment — refactoring is a journey, mistakes happen, and tests make the retreat cheap — and reverts to the earlier shape, adding a small walkValue closure to remove the duplicated walk(value.Interface(), fn) calls inside each branch.
Closing: testing what's genuinely knowable, then two more kinds. Maps also expose a testing problem: Go doesn't guarantee iteration order, so the map case can't live in the order-sensitive table and moves to its own subtest using an assertContains helper. The author then sharpens the point about weak assertions — assertContains alone would still pass if walk visited an entry twice or skipped one, so since length is predictable regardless of order, an assertLength check is added alongside. Readers are also nudged to break the test deliberately to see its failure message, keeping the TDD discipline intact even in a non-table test. Channels follow, drained with Recv() until closed (the if/else loop later tightened into a single for with the receive in both the init and post statements), and finally functions: zero-argument only, invoked via Call(nil) with walk applied to each returned value. The wrap-up lists what was covered — a slice of the reflect package, recursion over arbitrary structures, and a bad refactor survived without drama — points at the Go blog for more, and ends on the same note it opened with: now that you know reflection, do your best to avoid it. A closing appendix flags a real limitation left as an exercise — cyclic pointer structures will stack-overflow — and sketches the fix: track visited addresses in a map[uintptr]bool via reflect.Value.Pointer(), threaded through an internal helper that the public walk wraps.
Key claims, with the lines that support them
Using interface{} buys flexibility by throwing away type safety, for the caller as much as for the function's author.
The author's example: if you mean to pass a herd's species (a string) but accidentally pass its count (an int), nothing complains at compile time. Callers also lose the documentation value of a precise parameter type, while the author is left having to work out at runtime what was handed over.
“As a user of a function that takes interface{} you lose type safety.”
Reflection › So why not use interface{} for everything and have really flexible functions?Read in context →
Deciding what a value is before deciding what to do with it is what makes the traversal easy to extend.
This follows from the failures that came first: pointers and then slices both crashed on NumField, forcing the author to unwrap with Elem() and then branch on kind rather than assume a struct. Once the switch existed, arrays cost nothing — reflect.Array was simply added alongside reflect.Slice in the same branch.
“Let's rework the code so instead we check the type first and then do our work.”
Reflection › RefactorRead in context →
Tests make a wrong abstraction cheap to walk back, so it is worth trying one.
The author factored the loop into a shared count-plus-accessor pair, then hit maps, whose values cannot be fetched by index at all. Because the suite was already green, the abstraction was simply reverted to the earlier switch plus a small walkValue helper, and the chapter closes by calling the refactor bad in hindsight without treating it as a setback.
“Refactoring is a journey and sometimes we will make mistakes. A major point of TDD is it gives us the freedom to try these things out.”
Reflection › RefactorRead in context →
2 more claims
The right conclusion from learning reflection is to steer clear of it.
The chapter earns this by showing the costs along the way — lost compile-time guarantees, runtime checks, unreadable code — and admits it has only shown a small corner of the reflect package, pointing readers to the Go blog for the rest rather than encouraging wider use.
“Now that you know about reflection, do your best to avoid using it.”
Reflection › Wrapping upRead in context →
The finished walk is still incomplete: it dies on data that points back at itself.
A Person whose Friend field is set to its own address makes the function recurse forever and end in a fatal stack overflow. The author sketches the standard remedy — a map of already-visited pointer addresses, carried by an internal helper that the public walk sets up — but leaves it as an exercise rather than implementing it.
“Because the map needs to live across the whole traversal you'll want an internal helper that carries it as a parameter, with the public walk creating the map and calling in.”
Reflection › Known limitation: circular referencesRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Accepting any type pushes checks to runtime
A parameter declared as interface{} accepts every value, so the compiler can no longer verify what callers pass or what the body may do with it. The author must therefore inspect the value's type while the program runs, which is why such functions end up clumsy, harder to read and slower.
Mechanism 2
Branch on kind, then do the work
Each reflect.Value method only works for certain kinds — NumField panics on a pointer or a slice, and maps can only be read by key. Reading val.Kind() first and switching on it puts every type's access pattern in its own branch, so supporting a new kind means adding a branch rather than reworking the traversal.
Mechanism 3
Why pointer cycles never terminate
Recursive traversal follows a pointer field back into a value it already entered, so the same fields are visited forever until the stack is exhausted. Recording each pointer's numeric address in a map and skipping addresses already seen cuts the loop, but the map must be threaded through every recursive call.
Common misreadings
interface{} is how you write polymorphic functions in Go, so reach for it whenever a function should accept several types.
When several types should share a function, define a real interface listing the methods the function needs; callers then opt in by implementing them and keep compile-time checking. Reserve interface{} and reflection for the cases where no such method set can express the requirement.
“As a user of a function that takes interface{} you lose type safety.”
Reflection › So why not use interface{} for everything and have really flexible functions?Read in context →
Since Go gives no ordering guarantee for maps, the map test can only check that certain values turned up somewhere.
Order is the only thing iteration makes unpredictable; the number of values collected is fixed whatever the order. The author therefore keeps a presence check for each value and adds a length assertion, because presence checks alone would still pass if an entry were visited twice or one were skipped.
“assertContains alone would still pass if walk visited a map entry twice, or missed one out and only checked the remaining entries - as long as the specific values we search for are present somewhere, however many times.”
Reflection › One final problemRead in context →
Key concepts in Go fundamentals
- Reflection
- A program examining its own structure, especially types, at runtime. Inspecting values' types and fields at runtime via the reflect package, useful but best avoided.
- Where it applies: Clumsy, hard to read and slower; use only when a normal interface cannot express the need. This chapter covers only a small aspect of it; the Go blog goes deeper.
- Interface
- interface{} (aliased any) accepts any value, trading away compile-time type checks.
- Where it applies: Prefer a real method-based interface for polymorphism rather than interface{}.
- Table driven tests
- One test looping over a slice of named cases so new scenarios are cheap to add.
- Where it applies: Unsuitable when order is unguaranteed, as with maps, which needed a separate subtest.
- Circular references
- A struct holding a pointer back to itself, so recursive traversal never ends.
- Where it applies: Only affects pointer cycles; walk stack-overflows rather than erroring gracefully.
- Visited-pointer tracking
- Keeping a map of pointer addresses already seen so the traversal skips repeats.
- Where it applies: Needs an internal helper carrying the map, with public walk creating it.
0 more concepts
Reader’s notes on Go fundamentalsAI reading companion
“In short only use reflection if you really need to.”
Read in context →The chapter spends pages teaching a tool while repeatedly begging you not to use it. That ambivalence is honest, and rarer than it should be in tutorials.
“fn("I still can't believe South Korea beat Germany 2-0 to put them last in their group")”
Read in context →The joke is also the lesson: a deliberately absurd fake forces the next test to exist. If you'd written something plausible you might have believed you were done.
“We look at the first and only field. However, there may be no fields at all, which would cause a panic.”
Read in context →Note he names the bugs he's knowingly shipping instead of hiding them. Red-phase code gets amnesty, but only if you keep the list.
6 more notes
“it's just the keys are unknown at compile time”
Read in context →This line is where the elegant abstraction dies. Maps aren't index-addressable, and the "numberOfValues + getField" trick was secretly a bet that everything is.
“Refactoring is a journey and sometimes we will make mistakes.”
Read in context →The most valuable paragraph in the chapter, and it's about feelings rather than code. Reverting a clever refactor is cheap only because tests made it cheap.
“assertContains alone would still pass if walk visited a map entry twice”
Read in context →Sharp distinction: giving up on order doesn't mean giving up on cardinality. Most people drop both the moment a map appears.
“Non zero-argument functions do not seem to make a lot of sense in this scenario.”
Read in context →A hand-wave dressed as reasoning. val.Call(nil) will panic on any function with parameters — the real answer is "we chose not to handle it," and no test guards that edge.
“Now that you know about reflection, do your best to avoid using it.”
Read in context →A fine closing line, but it leaves the deeper point unsaid: walk is a one-way visitor with no error returns, no depth limit, and silent skipping of unhandled kinds. Reflection code rots at the edges, not the center.
“The standard approach is to track which pointer addresses have already been visited”
Read in context →Honest to flag it, but a cycle crash isn't a footnote — it's the first thing real-world data hands you. Also note unexported fields would panic on .Interface(), which the chapter never mentions at all.
A reader’s takeAI reading companion
Preserved failures teach more than polished success ever could
A well-paced chapter whose real subject isn't reflection at all but the psychology of iterative design: write the absurd fake, name your known sins, try the clever abstraction, and revert without shame when maps refuse to be indexed. The failed refactor is the chapter's best teaching moment precisely because it's preserved rather than edited out of history. Where it's weakest is at the boundaries — val.Call(nil) panics on parameterized functions, .Interface() panics on unexported fields, channels get drained as a side effect, unhandled kinds vanish silently, and cycles blow the stack (relegated to an exercise). Those aren't nitpicks; they're exactly the runtime surprises that justify the chapter's own closing advice to avoid reflection. The author earns the right to say "don't use this" by showing how quickly the code stops being pretty once reality arrives, but he stops just short of showing how badly it fails when reality is hostile.
Read Go fundamentals with these cards beside each passage.
Open in FeedLitraGo fundamentals: Sync
How would you protect two fields that must stay consistent together?
How Go fundamentals unfoldsAI-assisted
Opening: a trivially simple counter, built the TDD way. The chapter sets up a deliberately modest goal — a counter that's safe for concurrent use — and announces its own plan up front: build an unsafe version first, prove it works single-threaded, then expose its unsafeness and repair it. The first cycle is pure routine, and that's the point. A test asserts that three Inc() calls leave the value at 3; the compiler errors walk the reader from an undefined type, to an empty struct, to stub methods, to a real failure (got 0, want 3), and finally to the obvious fix of an int field incremented on each call. A brief refactor pulls the assertion into an assertCounter helper, which looks like tidiness but is really groundwork — that helper becomes a trap later.
Development: the failing concurrency test and the anatomy of a race. With the single-threaded version green, the stated new requirement arrives and the chapter writes the test that breaks it: a thousand goroutines each calling Inc(), coordinated by sync.WaitGroup (Add to set the count, Done per goroutine, Wait to block until all finish) so the assertion happens only after every increment has been attempted. The result is a short count — the text's example is 939, with the caveat that your number will differ. The explanation is the chapter's conceptual core: c.value++ is not one operation but read, increment, write, and the runtime may switch goroutines between any two of them. A worked interleaving shows two goroutines both reading 0, both computing 1, both writing 1, so one increment vanishes. The fix is sync.Mutex, with Lock() and a deferred Unlock() around the increment, so each goroutine waits its turn.
The turns: two warnings about how you wire the fix in. Having solved the problem, the chapter immediately complicates two tempting shortcuts. The first is embedding sync.Mutex in the struct so you can write c.Lock() directly; the text concedes it reads elegantly and then rejects it flatly, because embedding promotes the embedded type's methods into your public API, letting callers lock and unlock your internal state and inviting coupling you never wanted. The second is subtler: go vet reports that assertCounter copies a lock value, since a Mutex must not be copied after first use. The remedy is to take a *Counter instead, which in turn motivates a NewCounter() constructor that signals to users how the type should be created.
A lighter alternative, then the summing up. Before closing, the chapter offers a different answer for the narrow case it has been working in: sync/atomic. Because a counter is a single integer, an atomic.Int64 field with Add(1) and Load() removes the mutex and the lock/unlock discipline entirely while passing the same test unchanged — typically faster, and harder to misuse than the older pointer-passing atomic functions. The dividing line it draws is explicit: atomics for one value, a mutex once you must keep several fields or an invariant between them consistent. The wrap-up then recaps Mutex, WaitGroup, and sync/atomic, and widens out to the locks-versus-channels question, citing the Go wiki and the warning against over-using channels and goroutines simply because you can — channels for passing ownership of data, mutexes for managing state. It ends on the two practical habits the middle of the chapter earned: keep go vet in your build scripts, and don't embed a type just because it's convenient.
Key claims, with the lines that support them
An increment statement that looks like one indivisible action is not, so concurrent use quietly loses updates.
The author decomposes the increment into read, add and write, notes the runtime may interleave goroutines between those steps, and walks through two goroutines that each read zero and both write one — two calls yielding a total of one. He then demonstrates it empirically: launching a thousand goroutines produces a count like 939 instead of 1000, with the caveat that the exact shortfall differs on every run.
“c.value++ looks like a single, indivisible operation, but it isn't.”
Sync › Why does this happen?Read in context →
Guarding the increment with a mutex fixes the problem by forcing goroutines to take turns.
Having first shown the failure, the author adds a mutex field and brackets the increment with Lock and a deferred Unlock; the previously failing thousand-goroutine test now passes, because each goroutine must wait for the lock before touching the value.
“What this means is any goroutine calling Inc will acquire the lock on Counter if they are first. All the other goroutines will have to wait for it to be Unlocked before getting access.”
Sync › Write enough code to make it passRead in context →
Embedding the mutex in the struct may look tidier, but it is the wrong choice.
The author concedes the embedded version reads more elegantly, then argues against it: embedding promotes the embedded type's methods into your own public interface, so Lock and Unlock become part of your API. Anything public invites callers to couple to it, and a caller releasing your lock at the wrong moment would produce unpredictable bugs that are very hard to trace.
“Exposing Lock and Unlock is at best confusing but at worst potentially very harmful to your software if callers of your type start calling these methods.”
Sync › I've seen other examples where the sync.Mutex is embedded into the struct.Read in context →
2 more claims
Even with the lock in place, handing the counter around by value is unsafe; pass a pointer and build it through a constructor.
go vet reports that the assertion helper copies and passes a lock by value, and the mutex documentation states a mutex must not be copied once used — passing the struct by value duplicates the lock. The author changes the helper to take a pointer and adds a constructor returning a pointer, which also signals to readers that they should not hand-initialise the type.
“When we pass our Counter (by value) to assertCounter it will try and create a copy of the mutex.”
Sync › Copying mutexesRead in context →
Pick the concurrency tool by the shape of the data you are protecting rather than by habit.
As a counterpoint to the lock version, the author rewrites the counter with an atomic integer: the lock disappears, the same concurrency test still passes, and the update is made indivisible by low-level CPU instructions, which is typically faster. He bounds the advice explicitly — atomics suit a single value, a mutex is for several fields or an invariant that must hold across them, and channels are for handing over ownership of data.
“Use channels when passing ownership of data”
Sync › When to use locks over channels and goroutines?Read in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
How a lost increment happens
Incrementing a field is really three separate steps: read the current value, add one to a local copy, write it back. The runtime can switch goroutines between any of them, so two goroutines may both read the same starting number and then both write back the same result. Two calls, one visible increase — the other is silently discarded.
Mechanism 2
Why a lock makes it correct
A mutex lets only one goroutine hold it at a time; whoever arrives first acquires it and everyone else blocks until it is released. Wrapping the read-modify-write sequence in Lock and a deferred Unlock turns those three steps into one indivisible stretch, so no other goroutine can read a stale value in the middle of someone else's update.
Mechanism 3
How WaitGroup makes the test meaningful
The test announces up front how many goroutines it expects, each goroutine reports completion when it finishes, and the main goroutine blocks until the count reaches zero. Without that barrier the assertion could run while increments were still in flight, so the wait is what makes a wrong total real evidence of a race rather than of early measurement.
Common misreadings
Once the mutex is added and the concurrency test goes green, the counter is safe to copy and pass around like any other struct.
A passing test does not cover copying: duplicating the struct duplicates the lock, which the mutex documentation forbids after first use, and go vet flags exactly that in the test helper. The author's remedy is to pass a pointer and construct the counter through a constructor so callers never make a copy.
“When we pass our Counter (by value) to assertCounter it will try and create a copy of the mutex.”
Sync › Copying mutexesRead in context →
Goroutines and channels are Go's signature concurrency feature, so falling back on a mutex is unidiomatic or a beginner's shortcut.
The author says the common newcomer mistake runs the other way — reaching for channels and goroutines simply because you can. Go deliberately leaves both tools available: use channels when you are passing ownership of data, and a mutex when you are managing shared state.
“Use channels when passing ownership of data”
Sync › When to use locks over channels and goroutines?Read in context →
Key concepts in Go fundamentals
- Race condition
- Two goroutines read, change and write the same value so one update is silently lost.
- Where it applies: Only arises with unsynchronised concurrent access to shared mutable state.
- Mutex
- A lock letting only one goroutine touch the guarded data at a time.
- Where it applies: Must not be copied after first use, and should not be embedded into a public struct.
- WaitGroup
- A counter that blocks until every launched goroutine reports it has finished.
- Where it applies: Coordinates completion only; it does not protect shared data.
- sync/atomic
- Types like atomic.Int64 that make single-value updates safe with CPU instructions instead of a lock.
- Where it applies: Fits one value; multiple fields with a shared invariant still need a Mutex.
- Embedding
- Putting a type directly in a struct, which promotes its methods into your public API.
- Where it applies: Harmful when the promoted methods, like Lock and Unlock, should stay private.
0 more concepts
Reader’s notes on Go fundamentalsAI reading companion
“This should be trivial for Go experts like us.”
Read in context →The wink is earned here — the book has been building this "we" since chapter one. It also quietly insures against boredom: yes, this part is dumb, stay with me.
“tmp := c.value // 1. read”
Read in context →This is the whole chapter's center of gravity. Decomposing ++ into three lines is the one move that turns a flaky test number into a mechanism you can reason about.
“The test will probably fail with a different number”
Read in context →A test that fails differently every run is epistemically strange — it can't be your red in red-green-refactor, because green could also be luck. The chapter never admits that go test -race is the actual tool for this.
5 more notes
“this is bad and wrong”
Read in context →Blunt, and I like that he doesn't hide behind "it depends." But the stated reason is API surface, not mutexes specifically — the real lesson is about embedding, with the mutex as a vivid worst case.
“call of assertCounter copies lock value”
Read in context →The best beat in the chapter: the tests passed, and the code was still broken. A green suite as false comfort, caught by a tool rather than an assertion.
“it would be better to not initialise the type yourself”
Read in context →A constructor is documentation enforced by habit, not by the compiler — Counter{} still compiles fine. Go gives you no way to actually forbid it, so the fix is social.
“func (c *Counter) Value() int64”
Read in context →Quietly a breaking change — the return type shifted from int, so assertCounter and every caller must follow. Presented as a drop-in alternative when it isn't quite.
“Use channels when passing ownership of data”
Read in context →The most useful sentence in the chapter and it arrives as a footnote. Mutex-vs-channel is a design question; the counter example is too small to let you feel the difference.
A reader’s takeAI reading companion
A passing test suite is a floor, not a ceiling
A deliberately small chapter: the counter is almost too trivial to be interesting, and that's the point — it isolates one phenomenon (lost updates) so cleanly that the read-increment-write decomposition lands as genuine understanding rather than a rule to memorize. The chapter's real pedagogy isn't the Mutex, which is three lines; it's two moments of discomfort. First, the test that fails with a different number each run — a red you can't trust, which the chapter uses without quite acknowledging that it has broken its own TDD contract (and never mentions -race, the tool actually built for this).
Second, go vet catching a copied lock in code whose tests were already green — the strongest beat here, because it teaches that a passing suite is a floor, not a ceiling. The polemical turn against embedding is refreshingly unhedged but slightly overreaches: the argument is about public API surface in general, with the mutex serving as the scariest instance. The sync/atomic section reads as a later addition and shows the seam — "the same test passes unchanged" isn't true once Value() returns int64. And the richest idea, channels for ownership versus mutexes for state, is parked in the closing notes where a counter example can't illustrate it.
Continue reading
Still: a chapter that makes you distrust the word "single operation" forever has done its job.
Read Go fundamentals with these cards beside each passage.
Open in FeedLitraGo fundamentals: Context
How would Store propagate the context to its own slow dependants?
How Go fundamentals unfoldsAI-assisted
Opening: a happy path, deliberately too simple. The chapter frames the problem in operational terms — long-running work kicked off by a request that may be abandoned, and the debugging misery that follows if nobody tells that work to stop. It then hands you a minimal working system: a Server function that takes a Store (an interface with just Fetch() string), returns an http.HandlerFunc, and writes the fetched data to the response. A SpyStore test double and a single passing test establish the baseline. The author's point is that this is the easy case, and the interesting engineering begins when the user walks away mid-request.
First development: cancellation as an explicit method call. Following strict TDD, the author adds Cancel() to the Store interface, makes the spy sleep 100ms and record whether it was cancelled, and writes a failing test that derives a cancellable context from the request, schedules cancel via time.AfterFunc(5ms), and attaches it with request.WithContext. A short digression from the Go blog explains why contexts form a tree and why derivation matters for propagation. Then comes the chapter's most instructive moment: the minimal code that passes is to call store.Cancel() unconditionally before fetching — obviously wrong, yet green. Rather than treating this as a failure of TDD, the author treats it as a diagnostic: the test suite was underspecified. Adding an assertion to the happy-path test that the store was not cancelled forces a real implementation — run Fetch in a goroutine writing to a buffered channel, then select between that channel and ctx.Done(), racing the two outcomes. A refactor moves the cancellation assertions onto the spy as helper methods with t.Helper().
The turn: who should own cancellation? Having made the approach work, the chapter immediately questions it. Should a web server be manually cancelling its store? What if the store has its own slow dependencies — must Cancel hand-propagate down the whole tree? Citing the context package docs and Google's internal rule that a Context be the first parameter on every function between incoming and outgoing requests, the author pivots to passing the context into the store and letting it, and its dependants, take responsibility. Notably, he pauses to invite the reader's unease about that ergonomic cost rather than selling it.
Second development: rebuilding under the new design. Store.Fetch becomes Fetch(ctx context.Context) (string, error), the handler is emptied, and the spy is rewritten to behave like real context-aware code — a goroutine assembling the response one character at a time, checking ctx.Done() in a select on each step, with an outer select racing completion against cancellation. The happy path is fixed first with a two-line handler. Then, because httptest.ResponseRecorder can't report whether anything was written, the author rolls a custom SpyResponseWriter implementing all three http.ResponseWriter methods and flipping a written flag. The failing test becomes "no response should be written when cancelled," and the fix is simply to check the error from Fetch and return early. The handler ends up smaller than the version it replaced — the payoff of delegating cancellation downstream.
Closing: a summary, then a pointed caveat. The wrap-up lists the takeaways (testing cancelled requests, using context for cancellation, writing context-aware functions with goroutines/select/channels, propagating request-scoped context, hand-rolling a ResponseWriter spy). The chapter then appends an opinionated coda on context.Value, quoting Michal Štrba's joke about firing anyone who uses it: an untyped map sacrifices type safety and couples modules through shared keys, so values a function needs belong in typed parameters. The author concedes a narrow exception for orthogonal data like trace IDs, endorsing Jack Lindamood's formulation that Context.Value should inform rather than control — content for maintainers, never required input. It ends by recommending Štrba's "Context should go away for Go 2," agreeing that pervasive context-passing smells like a language-level gap, while noting that until that changes you need context anyway.
Key claims, with the lines that support them
Writing only the bare minimum code to pass a test is worth the discomfort, because an implementation that is obviously wrong yet still passes points at a hole in the tests.
To satisfy the cancellation test the author first wrote a handler that cancels the store on every single request — plainly wrong, yet green. That exposed that the happy-path test never checked the store was left alone, so he added an assertion that nothing was cancelled; only with both tests in place was a sensible implementation forced out.
“By being disciplined it highlighted a flaw in our tests and this is a good thing!”
Context › Write enough code to make it passRead in context →
A web handler should not be in the business of manually cancelling its collaborators; it should pass the request's context down and let each dependency cancel itself.
The author first builds a handler that owns cancellation, then breaks his own design by asking what happens when the store has slow dependencies of its own — its Cancel method would have to forward cancellation by hand to everything beneath it. Context already offers one consistent way to do that, and Go's own documentation plus Google's internal rule of taking a Context as the first parameter of every function between an incoming and an outgoing request describe exactly this propagation discipline.
“We can see after this that the server code has become simplified as it's no longer explicitly responsible for cancellation, it simply passes through context and relies on the downstream functions to respect any cancellations that may occur.”
Context › Write enough code to make it passRead in context →
Once responsibility moves downstream, the handler shrinks to almost nothing.
After the redesign the handler just calls the store with the request's context, returns silently if an error comes back, and otherwise prints the data. The test for that path needed a hand-written stand-in for the response writer, since the standard recorder cannot tell you whether anything was written at all — and it confirmed that a cancelled request produces no response.
“We can see after this that the server code has become simplified as it's no longer explicitly responsible for cancellation, it simply passes through context and relies on the downstream functions to respect any cancellations that may occur.”
Context › Write enough code to make it passRead in context →
2 more claims
A test double for a context-aware function has to behave like the real thing, cancellation included, or the test proves nothing.
The author's fake store does not simply sleep: it appends the answer character by character in a goroutine, checks the done channel on every iteration, logs when it is cancelled, and uses its own select to return either the finished string or the context's error. He points out that your own context-accepting functions will look much the same, so the pattern is worth understanding rather than copying.
“We can see after this that the server code has become simplified as it's no longer explicitly responsible for cancellation, it simply passes through context and relies on the downstream functions to respect any cancellations that may occur.”
Context › Write enough code to make it passRead in context →
Data a function actually needs belongs in typed parameters, not stuffed into a context.
Context values are nothing but an untyped map, so nothing is checked at compile time, every lookup might come back empty, and the keys create a hidden coupling between modules that breaks the moment someone edits one. Typed parameters are statically checked and self-documenting. The author allows one exception, quoting the rule that context values should inform rather than control: things orthogonal to the request, such as a trace id, can ride along instead of cluttering every signature.
“In short, **if a function needs some values, put them as typed parameters rather than trying to fetch them from context.Value”
Context › What about context.Value ?Read in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Racing a slow job against a cancel signal
A context exposes a channel that closes when the request is abandoned, and the slow fetch is run in a goroutine that writes its result to another channel. A select statement waits on both, so whichever finishes first decides the outcome: either the response is written, or the cancellation branch runs and no data is sent.
Mechanism 2
Why derived contexts stop everything
Contexts form a tree: each new context is derived from the request's own, so cancelling an ancestor cancels every descendant. Because every layer on the call path hands its context down, one cancellation at the top reaches the deepest worker without any layer needing to know what the layers below it depend on.
Mechanism 3
Cancellation only works if workers check
A context does not forcibly kill anything; it merely signals. The chapter's fake store builds its result one character at a time and tests the done channel on every step, returning early when the signal arrives. Without such a check inside the slow loop, the select in the caller would return while the work kept burning resources.
Common misreadings
Context looks like a handy bag for carrying whatever a deeper function might need, so passing request data through it saves threading parameters everywhere.
Treat context as a channel for cancellation, deadlines and request-scoped extras that no documented result depends on. Anything a function genuinely requires should be an explicit typed argument, because the value map offers no type safety, may not hold your key, and ties modules together through shared key names that break silently.
“In short, **if a function needs some values, put them as typed parameters rather than trying to fetch them from context.Value”
Context › What about context.Value ?Read in context →
Handing r.Context() to the store is enough — once the client disconnects, the slow work stops by itself.
A cancelled context only closes a channel; stopping work requires the slow code to watch for that signal and bail out. In the chapter the fake store checks the done channel inside its own loop and returns the context's error, and the handler still has to notice that error and refrain from writing a response.
“We can see after this that the server code has become simplified as it's no longer explicitly responsible for cancellation, it simply passes through context and relies on the downstream functions to respect any cancellations that may occur.”
Context › Write enough code to make it passRead in context →
Key concepts in Go fundamentals
- Context
- A value passed down a call chain that signals cancellation to everything derived from it.
- Where it applies: Good for cancellation and deadlines; not a place to smuggle required values.
- Spy
- A stand-in collaborator that records what was done to it so tests can assert on it.
- Where it applies: Must behave enough like the real thing; here the spy had to honour ctx.Done itself.
- Channel
- A typed pipe used here to race a slow fetch against a cancellation signal.
- Where it applies: Racing with select only works if the slow worker actually checks for cancellation.
0 more concepts
Reader’s notes on Go fundamentalsAI reading companion
“store.Cancel() fmt.Fprint(w, store.Fetch”
Read in context →The deliberately absurd implementation is the chapter's best teaching move: a passing test that's obviously wrong proves the test suite, not the code, was underspecified.
“By being disciplined it highlighted a flaw in our tests and this is a good thing!”
Read in context →Worth noting the asymmetry: TDD didn't find a bug in the code, it found a missing constraint. Tests as specification, not verification.
“Does it make sense for our web server to be concerned with manually cancelling Store?”
Read in context →This is the real pivot of the chapter. The first solution works and passes; it gets thrown out on design grounds alone. Rare honesty about green tests not being the finish line.
6 more notes
“(Pause for a moment and think of the ramifications of every function having to send in a context, and the ergonomics of that.)”
Read in context →The author plants the doubt before teaching the practice. Teaching the idiom while refusing to pretend it's elegant — that's unusual restraint for a tutorial.
“Feeling a bit uneasy? Good.”
Read in context →Pedagogically shrewd, but it sidesteps the obvious question: if the viral-parameter cost is real, what's the alternative inside today's Go? The answer is "none," and the chapter never quite says so until the footnotes.
“data, err := store.Fetch(r.Context()) if err != nil { return // todo: log error however you like”
Read in context →A silent return on cancellation means the client also gets 200 with an empty body for a genuine store failure. The test only checks "nothing written" — it can't tell cancellation from error. The todo is hiding a real gap.
“If you use ctx.Value in my (non-existent) company, you’re fired”
Read in context →Quoting a joke to make a real rule, then walking it back two paragraphs later with "but trace ids are fine." The actual principle — inform, not control — is the useful part; the bravado is noise.
“The content of context.Value is for maintainers not users.”
Read in context →Lindamood's line is the sharpest sentence in the chapter — a test you can actually apply: if removing the value changes documented behaviour, it shouldn't be in the context.
“we use Go's concurrency primitives to make two asynchronous processes race each other”
Read in context →The spy is now more intricate than the production code it stands in for. Honest about what writing context-aware functions costs, but it quietly concedes that testing cancellation means hand-building the very machinery you were trying to abstract.
A reader’s takeAI reading companion
A tutorial brave enough to demolish a passing test on design grounds
A chapter whose real subject isn't context at all — it's the moment when a passing test suite stops being evidence of good design. The structure is a deliberate two-act betrayal: act one builds a working manual-cancellation server through faithful TDD, act two throws it away because it's unidiomatic and pushes responsibility to the wrong layer. That demolition is the lesson, and it's braver than most tutorials dare be. The TDD showmanship (calling Cancel() before Fetch() to pass a test) is the best demonstration I've seen of tests-as-specification: the absurd implementation isn't a failure of discipline, it's discipline working, exposing a constraint the author forgot to state.
Where the chapter is weakest is where it's most honest: it tells you to feel uneasy about threading context through every signature, quotes Google's mandate, quotes Štrba's argument that the whole thing is a language-level deficiency — and then shrugs. The unease is never resolved because it can't be. Two things go under-examined. First, the final handler collapses cancellation and genuine store failure into one silent return, and the SpyResponseWriter test is structurally incapable of noticing; the "todo: log error" comment is doing a lot of load-bearing work. Second, by the end the spy is more complex than the code under test, which is an honest signal about the cost of context-aware APIs but also an unacknowledged tax on testability.
Continue reading
The context.Value section is the most quotable and least rigorous — a fireable-offence joke deployed as a rule, immediately undercut by a reasonable exception. The salvageable principle belongs to Lindamood, not the author: context values must inform, never control. That's a testable criterion; the rest is attitude.
Read Go fundamentals with these cards beside each passage.
Open in FeedLitraGo fundamentals: Roman Numerals
How would you encode the 3999 ceiling in the type system itself?
How Go fundamentals unfoldsAI-assisted
The chapter picks up Roman numerals as a deliberately "hard-looking" problem and refuses to treat it as one. The first test asks only that 1 convert to I, and the first implementation simply returns "I". That hard-coded answer is not an embarrassment to be apologised for — it is the move the chapter is arguing for. The point of the first cycle is to get out of the failing state as quickly as possible so that the API shape and the naming get pinned down before any algorithm exists. The discomfort of the hard-code is then put to work: because returning "I" is obviously wrong, it provokes the next test (2), which forces a single if, which in turn provokes the next, and only after several of these does any loop appear. Each step is small enough that the next one is obvious, and the growing pile of branches is the signal that tells you when to stop adding and start restructuring.
Development: moving the rules out of the code and into data
The turn in the implementation comes when the accumulating switch statement becomes too suspicious to keep. The author extracts the numeral rules into a small struct of value and symbol, collects those pairs into a slice ordered from the largest value down, and lets the conversion simply walk the slice — subtracting the value and appending the symbol while the remainder allows it. The payoff is the chapter's cleanest demonstration of why this separation matters: once the behaviour no longer encodes any particular number, supporting the whole range up to 3999 required no change to the algorithm at all, only extra rows in the table. Reaching something as intimidating as 1984 (MCMLXXXIV) involved no leap and no rewrite. The reverse direction is then built the same way — parsing a numeral back to a number using HasPrefix and TrimPrefix against the same table — which sets up a pair of functions that ought to be inverses of each other.
The turn: property tests attack the author's own design
Having two converters that pass their example tests, the chapter deliberately turns on them. A property-based round-trip test — convert a number to a numeral, parse it back, expect the original — is handed to quick.Check, and what it exposes is not an off-by-one in the algorithm but a flaw in the type signature. Logging the generated inputs produced values like 6849766357708982977 and −7028152357875163913: negatives and numbers vastly beyond the 3999 ceiling, none of which a Roman numeral can express at all. (On Linux, feeding the naive version such inputs can lock up the machine.) This is where the chapter's most-missed point sits. The fix is not simply swapping int for uint16; that only shrinks the invalid range, since everything from 4000 to 65535 remains expressible as a uint16 and still is not a Roman numeral. Closing the remaining gap requires an actual domain decision — return an error, or declare a dedicated type — and the chapter deliberately leaves that choice open rather than pretending the type system settled it. The value of these tests, on this reading, comes from being able to state domain rules in code, not from randomness finding bugs.
Closing: shape the input, don't weaken the check
The final technical move is a discipline about where to put a constraint. The tempting repair for out-of-range inputs is a guard inside the property — if arabic > 3999 { return true } — but that merely declares invalid cases passing and burns runs that test nothing. Instead the author supplies a custom value generator producing uint16(r.Intn(4000)), so all 1000 iterations land inside the valid domain and genuinely exercise the conversion. Shape the input rather than excuse it. The chapter then closes on its larger claim: intimidating problems become tractable if you begin with something trivial and advance in small increments, which the author reports is how he ships real systems — iterating outward from a "Hello world" website. That last part is offered as self-reported practice rather than measured evidence, and is worth reading as such. Appended is Dave's pedantic-but-load-bearing note that an int is a number, not an Arabic numeral: 0xFF == 255 holds, and the same quantity prints as 11111111, 255, 377, ff, or FF. Representation and quantity are distinct, which is exactly why describing this function's signature — and choosing its domain type — deserves the care the chapter spent on it.
Key claims, with the lines that support them
Hard-coding a result at the start of a test cycle is a legitimate move, because the point is to leave the failing state as fast as possible while the API and the rules get pinned down.
The converter first just returns "I", then grows a single if for 2, and only later acquires any loop; each uncomfortable hard-coded answer is what provokes the next test that forces more honest code.
“I know it feels weird just to hard-code the result but with TDD we want to stay out of "red" for as long as possible.”
Roman Numerals › RefactorRead in context →
Separating the numeral data from the conversion behaviour both removes the suspicious pile of branches and makes larger numbers work for free.
The switch statement is replaced by a small struct of value and symbol plus a slice iterated from the highest value downward; the author reports that supporting the full set of symbols up to 3999 required no algorithm change at all, only extending that slice.
“We've declared some rules around the numerals as data rather than hidden in an algorithm and we can see how we just work through the Arabic number, trying to add symbols to our result if they fit.”
Roman Numerals › RefactorRead in context →
Having built a converter that passes its example tests, the author then turns on it: a property-based round-trip test reveals that int is simply the wrong type for this domain.
Logging the generated inputs showed values like 6849766357708982977 and -7028152357875163913 — negatives and numbers vastly above the 3999 ceiling, none of which a Roman numeral can represent. The caveat is that moving to uint16 only shrinks the problem, since 4000 through 65535 still get through. (On Linux, running the naive version against such inputs can freeze the machine.)
“This is great! We've been forced to think more deeply about our domain which is a real strength of property based tests.”
Roman Numerals › Run our testRead in context →
2 more claims
Faced with inputs outside the valid domain, you should constrain what the test generates rather than excuse them inside the property.
A guard such as if arabic > 3999 { return true } hides the issue instead of addressing it; wiring the generator to produce uint16(r.Intn(4000)) instead means every one of the 1000 runs actually exercises the conversion, with no wasted iterations.
“You could guard against it with if arabic > 3999 { return true }, but that just papers over the problem: those iterations become no-ops.”
Roman Numerals › uint16Read in context →
Large, intimidating problems become workable if you start from something trivial and advance in small steps.
The author notes that reaching 1984 (MCMLXXXIV) involved no large leaps and no huge rewrites, and says this mirrors how he ships real systems — iterating outward from a "Hello world" website. This is self-reported practice rather than a measured result.
“The trick, as always, is to get started with something simple and take small steps.”
Roman Numerals › More TDD practice with iterative developmentRead in context →
How it works
The 2 mechanisms the chapter’s argument runs on.
Mechanism 1
Rules as data keep the algorithm fixed
The author moves the numeral rules out of a branching switch into a list of value/symbol pairs, and the conversion simply walks that list from the largest value down, subtracting and appending. Because the behaviour no longer encodes any particular number, covering every symbol up to 3999 meant only adding rows to the table, not touching the algorithm.
Mechanism 2
Fix the generator, not the assertion
When random integers exceed the Roman ceiling, the tempting repair is an early return true inside the property, but that merely declares out-of-range cases passing and burns runs that test nothing. Supplying your own value builder so arguments land in 0–3999 instead makes all 1000 iterations exercise the real conversion, shaping the input rather than weakening the check.
Common misreadings
The flaw the property test found was a type bug, so changing int to uint16 fixes it and the lesson is that random inputs find bugs.
Narrowing the type only shrinks the invalid range — everything from 4000 to 65535 is still expressible and still not a Roman numeral — so the remaining gap has to be closed by a domain decision such as returning an error or declaring a dedicated type, a choice the chapter deliberately leaves open. The value of these tests comes from being able to state domain rules in code, not from randomness itself.
“This is great! We've been forced to think more deeply about our domain which is a real strength of property based tests.”
Roman Numerals › Run our testRead in context →
Since the function converts an Arabic numeral into a Roman numeral, the int parameter is the Arabic numeral.
An int is a number; "Arabic numeral" names one way of writing numbers down. The author's appended note shows 0xFF == 255 holding true and the same value printing as 11111111, 255, 377, ff and FF — representation is separate from quantity, which makes the function's signature description worth phrasing carefully.
“We can write the same integer both as a hexadecimal and an Arabic (decimal) numeral.”
Roman Numerals › PostscriptRead in context →
Key concepts in Go fundamentals
- Property based tests
- Tests that throw random inputs at your code and check that domain rules always hold, instead of listing examples. Tests that state a rule about your domain and let the library generate many inputs to try to break it.
- Where it applies: Only as good as your understanding of the domain; random data alone is not the point. Useful only if you can express domain rules in code; they complement rather than replace example-based tests.
- Table driven tests
- A slice of input/expected cases looped over with subtests, so new cases cost almost no boilerplate.
- Where it applies: Best when every case is the same shape of "given X expect Y".
- Roman numeral subtractor rule
- Only I, X and C may sit to the left of a bigger symbol, and only before the next two higher symbols in their family.
- Where it applies: Rules out IC for 99 and ID for 499; combined with the 3-repeat limit it caps values at 3999.
- strings.Builder
- A type that builds strings efficiently with Write methods, minimizing memory copying.
- Where it applies: Normally reserved for real performance problems, but here it costs no extra code.
- quick.Config Values
- A function you give quick.Check to build the arguments yourself, so random input stays in the range you care about.
- Where it applies: Needed when the default generator produces values outside your valid domain; it shapes input, not the assertion.
- Type declaration from existing type
- Defining a new named type so invalid values cannot even be expressed in the API.
- Where it applies: Offered here as an alternative to returning an error; this chapter leaves the choice open.
0 more concepts
Reader’s notes on Go fundamentalsAI reading companion
“with TDD we want to stay out of "red" for as long as possible”
Read in context →Surely he means stay out of red for as short a time as possible, or: stay in green as long as possible. The sentence says the opposite of the practice it describes.
“Now use that uneasy feeling to write a new test”
Read in context →This is the real teaching of the chapter. Discomfort as a scheduling signal — the hard-coded return is a debt you feel, and the feeling tells you where the next test goes.
“We can always throw our code away and start afresh with the tests we have to guide us.”
Read in context →The quiet payoff of test-first: the code becomes disposable. Most anxiety about refactoring is really anxiety about not knowing when you've broken something.
10 more notes
“99 is not IC (it's XCIX, 90 + 9)”
Read in context →Notable that this rule, carefully explained, never shows up in the algorithm. The greedy table makes it structurally impossible to emit IC — the constraint is encoded by what's absent from the data, not by any check.
“Repetition in loops like this are usually a sign of an abstraction waiting to be called out.”
Read in context →The chapter's best move: it reads duplication as a message rather than a mess. Two near-identical branches aren't noise, they're a half-formed concept.
“We can refactor this by decoupling the data from the behaviour.”
Read in context →The switch and the table are computationally identical; what changes is where the domain knowledge lives. Extending to 1000 now means editing a list, not reasoning about control flow.
“It will likely freeze your system (requiring a hard reboot).”
Read in context →A warning sticker where a type should be. The naive loop allocating int64-length strings is exactly the bug the property test found — and the fix was uint16, not a caution box.
“The only way it could pass is if they have the same kind of bug”
Read in context →Honest about round-trip testing's blind spot: symmetric bugs survive. If both functions share the same wrong table, the property holds perfectly and says nothing.
“A lot of people think property based tests are mainly about random data but they would be mistaken.”
Read in context →Right, and the chapter proves it by accident: the randomness found nothing about Roman numerals, it found that int was the wrong type. The value was in being forced to state a property at all.
“those iterations become no-ops”
Read in context →Good catch, and it generalizes: a filter inside an assertion silently guts your sample size. 1000 runs with a >3999 guard is maybe a handful of real tests.
“Can you think of a way of making it so it's impossible for someone to call our code with a number greater than 3999?”
Read in context →The exercise that answers itself. If a constrained type existed, the Values generator and the guard both vanish — the test wouldn't need to know the domain's upper bound because the type would.
“The representation of a number as a numeral is independent of its properties as a number”
Read in context →Dave's pedantry earns its keep. ConvertToRoman is misnamed: nothing is converted, an int is formatted. The right analogue is Itoa, and naming it so would have made the uint16 question obvious much earlier.
“But every other version of the kata doesn't care about this distinction so”
Read in context →A shrug placed right after the most intellectually serious page in the chapter. The author prints the critique and declines it — generous, but the shrug is doing work the chapter elsewhere refuses to allow.
A reader’s takeAI reading companion
The algorithm never changes again once code becomes pure data
A chapter whose real subject isn't Roman numerals but the discipline of refusing to leap. Its strongest passages are the ones that treat code as a speaker: duplication in the loop 'tells you something', the switch is 'describing truths along with behaviour', the uneasy feeling after hard-coding 'I' is a scheduling signal for the next test. That's a genuinely useful stance, and the switch-to-table refactor is the clean payoff — the algorithm never changes again, only the data. The property-based section is more interesting for what goes wrong than for what it teaches. Randomness didn't discover anything about Roman numerals; it discovered that int was the wrong parameter type, which is a design finding, not a test finding.
And the chapter never quite closes the loop it opens: it painstakingly explains the subtractor-family rule (IC is not 99) that the greedy table satisfies only by omission, never by check; it bolts a system-freeze warning onto code where a narrower type belonged; it adds a custom generator capped at 3999 and then asks, as an exercise, whether a type could make >3999 impossible — when that type would delete both the generator and the earlier guard. Dave's postscript is the sharpest page: the function doesn't convert, it formats, and had it been named that way, the type question would have surfaced on page one.
Continue reading
The author reprints the critique and shrugs it off because 'every other version of the kata' does — a small failure of nerve in a chapter otherwise built on taking the code's discomfort seriously.
Read Go fundamentals with these cards beside each passage.
Open in FeedLitraGo fundamentals: Maths
How does the SVG origin at the top left change every coordinate calculation?
How Go fundamentals unfoldsAI-assisted
This chapter opens by refusing to treat drawing as a drawing problem. An analogue clock face rendered as SVG is framed, from the first page, as a real mathematics problem: where exactly do the hands point? The author pins down a concrete case before generalising — the clock centre sits at (150,150), the hands measure 50, 80 and 90 units, and possible parameters like clockface radius or image size are explicitly set aside until the simple version works. An acceptance test for hand coordinates is written against those fixed numbers, and from there the chapter TDDs downward: secondsInRadians first, then secondHandPoint on the unit circle. Here the chapter hits its first real obstacle, and it is not a design obstacle but an arithmetic one. Slicing a turn into fractions and multiplying by 30 leaves a residue, so the product no longer equals math.Pi exactly and the test fails on a correct implementation. The author's answer is deliberately cheap: rewrite the formula as π/(30/n) so the division stays inside and exact comparison is restored; where that inversion is impossible, absorb the leftover sin/cos drift with a 1e-7 tolerance. An arbitrary-precision library like math/big is treated as overkill for drawing a clock. With the angle settled, the unit vector is moved onto the canvas in a forced order — multiply by hand length to scale, negate Y to compensate for SVG's top-left origin where Y grows downward, add the centre to position — because flipping or shifting before scaling would multiply the correction too.
The middle of the chapter turns from maths to the quality of the tests themselves. Asserting that the output text contains a certain line ties the test to characters rather than to meaning: it passes happily when the document is not valid SVG at all, and it fails over a harmless extra space between attributes. So the string matching is replaced by unmarshalling the output into typed Go structs, which makes the test compare coordinate values and therefore fail only on real defects. On that firmer footing the minute and hour hands get built, and the method shifts from deriving each angle afresh to reusing the hand already finished: the minute hand divides secondsInRadians by 60 and adds the minute offset, because it moves a sixtieth as far per second, and the hour hand divides minutesInRadians by twelve because a full turn takes twelve hours rather than one. Duplication is pulled out into angleToPoint and makeHand. Alongside this, the step size visibly grows: the author admits boredom with minutesInRadians after only two cases, and later implements hourHandPoint as a single line composing angleToPoint and hoursInRadians. What changes, though, is granularity and not order — the cases are still written before the code, several at a time now that the design is understood, and the hour-radians test still caught a float-equality failure that forced a roughlyEqualFloat64 comparison. The tests are doing real work even at the larger stride.
The close finishes the clockface and then steps back. Hour-hand radians are compared with roughlyEqualFloat64, hourHandPoint and hourHand are drawn, and the magic numbers accumulated along the way are given names as constants. The final reflection makes three moves. TDD is a tool, not an obligation to be served. A program should ship with a library, so the work is usable beyond the one binary that prompted it. And — reversing the usual emphasis — the lasting knowledge about SVG lives in the test code rather than the production code: many different production techniques could emit the same drawing, so the strictest available statement of what a correct SVG is belongs in the test. That is precisely why the acceptance test earns its own file while the fine-grained angle tests stay inside the package. The code records only the how; the tests are where the knowledge keeps living.
Key claims, with the lines that support them
Start from one concrete case with fixed constants and generalise into parameters only after it works.
The author pins the clock centre at (150,150) and hand lengths at 50, 80 and 90, explicitly setting aside possible parameters such as clockface radius and image size until the simple version is done; the acceptance test is written against those fixed numbers.
“I could think about more parameters - the radius of the clockface circle, the size of the SVG, the colours of the hands, their shape, etc... but it's better to start off by solving a simple, concrete problem with a simple, concrete solution, and then to start adding parameters to make it generalised.”
Maths › An Acceptance TestRead in context →
Rather than deriving each new hand's angle from scratch, reuse the hand already built and divide by a fixed factor.
Having finished the second hand, the author gets the minute hand by dividing secondsInRadians by 60 and adding the minute offset, since the minute hand moves 1/60th as far per second; the hour hand then divides minutesInRadians by twelve because a full turn takes twelve hours rather than one.
“Rather than working out how far to push the minute hand around the clockface for every second from scratch, here we can just leverage the secondsInRadians function.”
Maths › Write enough code to make it passRead in context →
Following from that reuse, the size of a TDD step may grow with the author's confidence without giving up writing the test first.
After only two cases the author admits boredom with minutesInRadians and moves on, later implementing hourHandPoint in a single line that composes angleToPoint and hoursInRadians — but the cases are still written before the code, and the hour-radians test still caught a float-equality failure that forced a roughlyEqualFloat64 comparison.
“But also note: I'm not skipping writing the tests entirely - I'm still writing them first.”
Maths › On TDD ZealotryRead in context →
1 more claim
Reversing the usual emphasis, the lasting knowledge about SVG lives in the test code, not the production code.
The author reasons that many different production techniques could emit the same drawing, so the strictest statement of what a correct SVG is belongs in the test, which is why the acceptance test gets its own file while the fine-grained angle tests stay inside the package; the code holds only the how.
“The what of an SVG lives in our tests; the how in the code.”
Maths › The Most Valuable TestRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Why float equality breaks
In this chapter the author shows that slicing a turn into fractions and then multiplying by 30 leaves a residue, so the product no longer equals math.Pi exactly. Rewriting the formula as π/(30/n) keeps the division inside and restores exact comparison; where that inversion is impossible, a 1e-7 tolerance absorbs the leftover sin/cos drift.
Mechanism 2
Unit vector onto the SVG canvas
A point on the unit circle is a direction of length 1 with the angle measured from 12 o'clock. Multiplying by the hand length scales it, negating Y compensates for SVG's top-left origin where Y grows downward, and adding the centre (150,150) moves it into place. The order is forced: flipping or shifting before scaling would multiply the correction too.
Mechanism 3
Why string matching misreads SVG
Asserting that the output text contains a certain line ties the test to characters rather than meaning: it still passes when the document is not valid SVG, and it fails over a harmless extra space between attributes. Unmarshalling the output into typed Go structs makes the test compare coordinate values instead, so it fails only on real defects.
Common misreadings
Since floating point arithmetic keeps breaking these tests, you need an arbitrary-precision tool such as math/big.
Two cheaper routes are enough: rearrange the expression so the division happens inside, as π/(30/n), which restores exact equality with math.Pi, or define approximate equality with a 1e-7 threshold for points. The author treats a precision library as overkill for drawing a clock, and notes the rearrangement works only because that division can be inverted.
“There are two ways around this:”
Maths › Floats are horribleRead in context →
When the author says he is bored of testing a function and then writes the next one as a single line, he has quietly abandoned test-first development.
What changes is granularity, not order: the cases are still written before the implementation, just several at a time once the design is already understood. The payoff shows up when the hour-hand test still fails on float equality and forces a roughly-equal comparison, so the tests are doing real work.
“But also note: I'm not skipping writing the tests entirely - I'm still writing them first.”
Maths › On TDD ZealotryRead in context →
Key concepts in Go fundamentals
- Acceptance test
- A high-level test that passes only when the whole user-visible feature works, not just one function. A high-level test that drives the whole program's output, here the full SVG for a given time.
- Where it applies: It tells you when you are done, but it is too coarse to localise bugs; unit tests do that. Useful at the edges; low-level unit tests still carry the fine-grained angle maths.
- Unit circle
- A circle of radius 1 around the origin, where x = cos(angle) and y = sin(angle) give coordinates.
- Where it applies: Here the axes are swapped (x = sin, y = cos) because the angle is measured from 12 o'clock.
- Radian
- An angle unit where a full turn is 2π instead of 360 degrees; Go's math.Sin/Cos require it.
- Where it applies: Only matters at the boundary with the math package; the clock logic itself is fractions of a turn.
- Floating point inaccuracy
- Decimal arithmetic drifts, so dividing then multiplying by 30 no longer returns exactly math.Pi.
- Where it applies: Fine to tolerate for drawing; unacceptable if exact equality or high precision is required.
- Table driven tests
- One test body looping over a slice of time/expected-value cases with subtests named by helper functions. One test body looping over a list of time/expected-value cases.
- Where it applies: Helps once several similar cases exist; adds ceremony for a single case. Needs a helper like containsLine and local copies of unexported helpers across packages.
- XML unmarshalling
- Turning XML bytes into a Go struct so tests compare values, not characters.
- Where it applies: Only as precise as the struct tags and field types you declare.
4 more concepts
- Refactoring
- Removing duplication after green, e.g. angleToPoint and makeHand.
- Where it applies: Only done once tests pass; behaviour must stay identical.
- TDD cycle
- The write-test-then-code rhythm, used as a tool for feedback rather than an obligatory ritual.
- Where it applies: Steps may be taken in larger chunks once the author already knows the design, but tests still come first.
- Magic numbers as named constants
- Replacing bare numbers with names like secondsInHalfClock so the equations state their meaning.
- Where it applies: Useful for numbers with domain meaning; also leaves the door open to make them parameters.
- Program and library pairing
- Shipping the reusable calculation API alongside the executable that uses it.
- Where it applies: Only possible because test-driven isolation produced small named functions.
Reader’s notes on Go fundamentalsAI reading companion
“But not today! Today we'll use mathematics to solve a real problem.”
Read in context →The pep-talk opening is doing sales work — it concedes up front that math is usually irrelevant to the job, then stages this chapter as the exception. Honest, but note the framing: the math here is incidental; the real lesson is decomposition.
“it's better to start off by solving a simple, concrete problem with a simple, concrete solution”
Read in context →The actual thesis of the chapter, buried in an aside about parameters. Resisting premature generalization is harder than any trigonometry that follows.
“return Point{150, 60”
Read in context →Hardcoding the answer looks like cheating, and that's the point — it proves the test can fail and then pass, so the next test is the one doing real work. Still, most readers' instinct to skip this step is exactly what TDD is trying to break.
17 more notes
“π / (30 / numberOfSeconds)”
Read in context →Clever, but notice what just happened: the author rewrote the math to flatter the float representation. It's a workaround dressed as elegance — and it immediately introduces a division-by-zero that needs its own apologetic sidebar.
“Dividing by +Inf seems to result in zero”
Read in context →"Seems to" is doing a lot of load-bearing here. The fix works by accident of IEEE 754 semantics, not by design — a reader who copies this pattern elsewhere will eventually get burned.
“But given the objective is to draw an SVG and not land on the moon”
Read in context →The best line in the chapter. Precision is a requirement, not a virtue — and knowing which one you're in is engineering judgment.
“Yeah, I guess I screwed up. This feels wrong.”
Read in context →Rare and valuable: the author leaves the wrong turn in the text instead of editing it out. Showing SecondHand as a secretly-SVG-shaped function, then noticing the smell, teaches more than a clean solution would.
“I'm testing a data structure - XML - by looking at its representation as a series of characters”
Read in context →This generalizes far beyond clocks: assert against the parsed thing, not its rendering. Same error lives in every snapshot test and every regex over JSON.
“The %f format directive is printing our coordinates to the default level of precision - six decimal places.”
Read in context →A test failing on formatting, not logic. Good illustration that string-matching acceptance tests couple you to presentation details — the "fix" was to pin precision, which is a real decision smuggled in as a test repair.
“Do I really care about the style of the hand? Or, for that matter, the empty Text node that's been generated by zak?”
Read in context →The sharpest idea in the segment: the test's data model should encode only what you're asserting. Deleting fields from the struct is how you declare intent.
“Since they're unexported, they're not visible outside the package they were declared in, so we need our own copies here.”
Read in context →Duplicated helpers across test packages — accepted without a murmur right after a whole section preaching DRY. The black-box package boundary was worth the copy, but the author never says so out loud.
“Write tests until fear is transformed into boredom.”
Read in context →A coverage heuristic grounded in feeling rather than metrics. Honest, and unfalsifiable — boredom also arrives when you're confidently wrong.
“return (math.Pi / (6 / (float64(t.Hour() % 12))))”
Read in context →Written as division-of-division to echo the earlier formula's shape, when Pi*h/6 would be plainer — and the h=0 case only survives because float division by zero gives +Inf and Pi/Inf is 0. Tests pass; the reasoning is luck.
“Floating point arithmetic strikes again.”
Read in context →Third encounter, and still handled reactively — loosen the comparison when it bites. The reuse of minutesInRadians bought elegance and paid in accumulated error.
“I'm not going to plough through TDD ceremony for the sake of it.”
Read in context →The chapter's most useful admission: the ritual earns its keep only while it's teaching you something. Worth noting he can say this because he already paid for the knowledge three hands ago.
“these constants could easily become parameters. We're helping to leave that door open (even if we never go through it)”
Read in context →Naming as cheap optionality — honest about the fact that the door probably stays shut, which is the opposite of the usual premature-generality excuse.
“we've built a very reasonable little API for clockface calculations”
Read in context →The real payoff claim: testability forced decomposition, and decomposition accidentally produced a library. A good argument for TDD that has nothing to do with catching bugs.
“the most sophisticated piece of code for handling SVGs isn't in our application code at all; it's in the test code”
Read in context →He names the discomfort before the reader can voice it. A rhetorical move: pre-empt the objection, then reframe it as the lesson.
“The what of an SVG lives in our tests; the how in the code”
Read in context →The whole section compressed to seven words. This is the line worth stealing — and it quietly inverts the usual hierarchy where code holds the truth and tests merely check it.
“it doesn't matter how we produce our SVG, what is important is what we produce”
Read in context →True only because the test is strict. The freedom to refactor is purchased by the test's rigor — so the liberty he's celebrating is a consequence of the labor he's defending.
A reader’s takeAI reading companion
TDD with honest limits: write tests until fear becomes boredom
This chapter arrives wearing a disguise. It opens with a pep talk — maths is usually irrelevant to your job, but today it isn't! — and that sales pitch is the one claim the chapter never actually makes good on. The trigonometry is incidental, almost decorative; what the chapter really teaches is decomposition under pressure, and its true thesis is tucked into an aside early on: start with a simple, concrete problem and a simple, concrete solution. Resisting premature generalization turns out to be far harder than any unit-circle identity that follows.
The unfolding is a spiral, not a line. A hardcoded Point{150, 60} proves the harness can fail and pass; then seconds in radians, then a point on the unit circle, then scaling, flipping and translating that vector onto SVG space. Each hand repeats the cycle with less ceremony than the last, until the author openly declines to perform the ritual at all — the chapter's most useful admission, and one he's earned, because he paid for the knowledge three hands ago. TDD here is explicitly a tool with diminishing returns, not a creed, and 'write tests until fear is transformed into boredom' is an honest heuristic that is also quietly unfalsifiable: boredom also arrives when you're confidently wrong.
Continue reading
Two threads run underneath and deserve naming. The first is floating point, encountered three times and handled reactively every time. π / (30 / numberOfSeconds) is the math rewritten to flatter the representation — a workaround dressed as elegance, which immediately spawns a division-by-zero that survives only because dividing by +Inf 'seems to' give zero. The hour formula repeats the trick, and by the time roughlyEqualFloat64 appears, the pattern is clear: the tests pass, but part of the reasoning is luck wearing the costume of a clever identity. Reuse bought symmetry and paid in accumulated error. Copy these formulas elsewhere and you will eventually get burned; copy the loosened comparison and you'll at least be honest about tolerance. The redeeming line — 'the objective is to draw an SVG and not land on the moon' — is the chapter's best, because it reframes precision as a requirement rather than a virtue, and knowing which you're in is the whole of engineering judgement.
The second thread is the one that outlives clocks entirely: stop asserting against renderings. Matching SVG as a string couples you to %f's six decimal places, so a formatting change breaks a logic test; parsing into typed structs fixes that, and then pruning fields from those structs — do I care about the hand's style, or zak's empty Text node? — becomes a way of declaring intent in the data model itself. Every snapshot test and every regex over JSON commits the same sin. Worth noticing, too, that the DRY sermon stops politely at the package boundary: unexported helpers get copied into the black-box test package without comment. The trade was right; the silence about it is the gap.
The ending is where the chapter earns its keep, and it does so by pre-empting the reader's objection rather than hiding from it. Yes, the most sophisticated SVG code lives in the tests, not the application — and 'the what of an SVG lives in our tests; the how in the code' is the seven-word version worth stealing, because it inverts the usual hierarchy where code holds the truth and tests merely audit it. Note the circularity, though, and it's a virtuous one: the freedom to rewrite the renderer however you like is purchased entirely by the test's strictness. The liberty being celebrated is a consequence of the labour being defended. Same with the constants: naming magic numbers as cheap optionality, with an unusually honest admission that the door probably stays shut.
What to tell the reader. Don't come for the maths; come for the shape. Testability forced decomposition, and decomposition accidentally produced a small, reasonable clockface library — an argument for TDD that has nothing to do with catching bugs and everything to do with pressure on design. Keep the author's willingness to leave his own wrong turn in the text ('Yeah, I guess I screwed up') as the model: a visible mistake teaches more than a clean solution. But read the float sections with suspicion rather than reverence, assert against parsed structures rather than their printed form, and treat every loosened comparison as a decision you owe yourself an explanation for — not a repair.
Read Go fundamentals with these cards beside each passage.
Open in FeedLitraGo fundamentals: Reading files
How should the package behave if one file among many fails to parse?
How Go fundamentals unfoldsAI-assisted
Opening. The chapter begins not with a package but with a posture. The author wants to build blogposts, something that turns a folder of markdown files into a slice of Post values, and the first decision is where to stand while writing it. The answer is: on the consumer's side. Rather than sketching the abstractions the package will need and assembling them upward, the author writes the call he wishes he could make and lets that drive everything behind it. The justification is an argument about self-knowledge — our imagination at the start of a task is untrustworthy, and Kent Beck's line that optimism is programming's occupational hazard while feedback is its treatment is invoked to make the point that a short loop returning real information beats a long one built on guesses. The first concrete consequence is the test setup: instead of a real directory, the author hands in an in-memory file system, a map pretending to be files. This is partly about speed — no disk in the loop, no new fixture file per behaviour — and partly about reach, since failure modes that are awkward to stage on a real folder become trivial here.
Development. From there the chapter advances in the smallest slices that still produce a passing test. First the code merely reads the directory and counts what it finds; only then does parsing begin. Along the way the author keeps asking a narrowing question — not what was I handed? but what does this code actually use? The test file-system type is the first casualty: as long as the function demands it, the package works only inside its own tests, so the parameter widens to fs.FS. The same reasoning then runs one level down, shrinking fs.File to io.Reader, because io.ReadAll is the sole consumer and asks for nothing more. Parsing proceeds line by line — title, description, tags — and this is where the second mechanism of the chapter shows itself. With behaviour pinned by green tests, the author has license to experiment: the opaque offsets 7 and 13, which did nothing but get the job done, become named separator constants and then a readMetaLine helper built on strings.TrimPrefix. The claim is not that the safety net produces good design directly, but that it makes experiments cheap, and more cheap experiments per unit of time improve the odds of landing somewhere good.
Turn. The chapter's pivots are mostly acts of restraint. When the question of a failing file system arises, the author shows how easy the fake would be — a single method on a stub that always errors — and then declines to write the test, on the stated condition that the code does nothing with the error but pass it upward. That permission is explicitly conditional, not a general licence to skip error tests. The same discipline governs the end of the implementation work: body parsing is completed with a scanning loop, the loop is extracted into readBody, and the end-to-end slice runs. The author then lists what remains unhandled — malformed files, non-markdown extensions, metadata in a different order — and judges that these call for more tests but no change to the design already validated. This judgement is offered as judgement, not proof, and it is also where the chapter anticipates its most likely misreading: a passing happy path is a reason to keep going, not a reason to declare the package finished.
Close. The reflection turns the lesson outward. The author sets the finished production call beside the test call and notes that they read almost identically, differing only in which file system is handed in. That symmetry is the chapter's evidence for its final claim: consumer-driven, top-down TDD is worth using even for packages that will never leave your own project, because driving the design from the caller's side is what kept over-imagined abstractions out and left genuinely useful tests behind. The steel thread works, and what remains open is implementation detail rather than structure. A closing caution guards against over-reading io/fs: it describes reading only, with no writing and no OS-specific features, so anyone wanting to send posts back out should look at what the standard library already offers on that side — io.Writer and its relatives — and apply the same habit of accepting the narrowest interface the work requires.
Key claims, with the lines that support them
Work should be sliced iteratively toward what the consumer needs, instead of being built upward from abstractions you imagined before starting.
The author argues that our imagination at the start of a task is untrustworthy, and leans on Kent Beck's line that optimism is programming's occupational hazard while feedback is the treatment — so short loops that return real feedback beat speculative structure.
“We should not trust our over-active imaginations when we start work. We could be tempted into making some kind of abstraction that is only validated once we stick everything together, such as some kind of BlogPostFileParser.”
Reading files › Iterative, test-driven developmentRead in context →
Taking standard-library interfaces as arguments, rather than concrete types, keeps a package loosely coupled and reusable.
Following from that iterative start, the author shows the code only works inside its own tests while it demands the test file-system type, and fixes this by switching the argument to fs.FS; the same reasoning then shrinks fs.File to io.Reader, since io.ReadAll is the only consumer and it asks for nothing more.
“Learning to leverage interfaces defined in Go's standard library (e.g. io.fs, io.Reader, io.Writer), is vital to writing loosely coupled packages.”
Reading files › File system abstractions introduced in Go 1.16Read in context →
Having tests pass gives you permission to experiment during refactoring, which makes a good design more likely.
With the behaviour locked in, the author reworks the metadata parsing: the unexplained numbers 7 and 13 that merely 'got the job done' are replaced by named separator constants and then by a readMetaLine helper using strings.TrimPrefix — a change tried only because the safety net made it cheap.
“The TDD approach gives us this license to frequently experiment with ideas, so we have more shots at writing great code.”
Reading files › RefactorRead in context →
3 more claims
Not every error path needs its own test; where the code merely passes an error upward, skipping the test can be the pragmatic choice.
The author demonstrates that a failing file system is trivial to fake — one method is enough for a stub that always errors — and still declines to write the test, precisely because the code does nothing interesting with the error beyond returning it. The permission depends on that condition.
“In some cases, testing error handling is the pragmatic thing to do but, in our case, we're not doing anything interesting with the error, we're just propagating it, so it's not worth the hassle of writing a new test.”
Reading files › Error handlingRead in context →
Staying fixed on the goal is what produces the decisions that matter, rather than chasing edge cases early.
The author builds the shortest working end-to-end slice first and then lists what is still unhandled — malformed files, non-markdown extensions, metadata in a different order — judging that these need more tests but no change to the design already validated. The author presents this as a judgement, not something proven.
“Keeping focused on the goal means we made the important decisions, and validated them against the desired behaviour, rather than getting bogged down on matters that won't affect the overall design.”
Reading files › Iterating furtherRead in context →
Consumer-driven, top-down TDD is worth using even for packages that never leave your own project.
Turning the lesson outward at the close, the author points to the symmetry between the finished production call and the test call: they read almost identically and differ only in which file system is handed in. Driving the design from the caller's side is what prevented over-imagined abstractions and left genuinely useful tests behind.
“When you're creating packages, even if they're only internal to your project, prefer a top-down consumer driven approach. This will stop you over-imagining designs and making abstractions you may not even need and will help ensure the tests you write are useful.”
Reading files › Wrapping upRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Narrow interfaces loosen coupling
The author asks what the code actually needs, not what it happens to be handed: since the read step only feeds io.ReadAll, the parameter is widened from a concrete test file system to fs.FS and from fs.File to io.Reader. Because any provider satisfying those small standard interfaces now fits, the package stops depending on real folders and becomes usable outside its own tests.
Mechanism 2
Green tests as a license to experiment
Once the behaviour is pinned down by passing tests, changing the internals carries no risk of silent breakage, so the author can try ideas freely. That is how the opaque offsets 7 and 13 turn into named separators and then a strings.TrimPrefix helper: more cheap experiments per unit of time means a better chance of landing on a good design.
Mechanism 3
In-memory file system speeds feedback
A map pretending to be files removes the disk from the loop, so no new fixture file is needed per behaviour and the tests avoid file-system access. The author notes the cost of the alternative: extra files, slower runs, and failure modes that are hard to trigger on a real folder.
Common misreadings
The end-to-end slice passes all its tests, so the package is basically finished and ready for real use.
That slice deliberately covers only the happy path: files in the wrong format, files that are not markdown, and metadata written in a different order are all still unaddressed. The author's point is that these gaps call for more tests rather than a redesign — which is a reason to keep going, not a reason to call it done.
“Keeping focused on the goal means we made the important decisions, and validated them against the desired behaviour, rather than getting bogged down on matters that won't affect the overall design.”
Reading files › Iterating furtherRead in context →
Since the file system is now injected through io/fs, the same interface will cover writing posts back out as well.
io/fs describes reading only; it gives you no writing and no OS-specific features. For sending data out, the author directs readers to look at what the standard library already offers on that side, such as io.Writer, and to apply the same habit of accepting the narrow interface.
“Remember to keep thinking about what the standard library offers currently, if you're writing data you should probably look into leveraging existing interfaces such as io.Writer to keep your code loosely-coupled and re-usable.”
Reading files › Writing?Read in context →
Key concepts in Go fundamentals
- io/fs
- A Go 1.16 interface package describing a file system, so code can accept any provider instead of real OS folders.
- Where it applies: Only covers basic read-oriented file-system operations; it does not give you writing or OS-specific features.
- Test Double
- A stand-in implementation used in tests, here an in-memory map pretending to be files on disk.
- Where it applies: Useful while behaviour is interface-level; it cannot prove real OS file quirks behave the same.
- Sliming
- Hardcoding a fake return just to make a test pass, so you can design the shape before the logic.
- Where it applies: Only helpful when the structure is still unknown; here the structure already existed, so it was skipped.
- Iterator
- Here, bufio.Scanner walking the file one text line at a time so each metadata field can be peeled off in order.
- Where it applies: Line-oriented only; structural validation of the file is deferred to later work.
- Steel thread
- The shortest end-to-end slice of working functionality through the happy path, built before edge cases.
- Where it applies: It is not production ready; bad formats, non-.md files and reordered metadata are still unhandled.
- strings.Builder
- A buffer you write lines into with fmt.Fprintln while scanning, then trim the trailing newline.
- Where it applies: Needed because bufio.Scanner strips newlines, so the final extra newline must be trimmed.
1 more concept
- Dependency Injection
- Passing the file system in, so tests hand over fstest data and production hands over a real directory.
- Where it applies: io/fs only supports reading; writing needs other interfaces such as io.Writer.
Reader’s notes on Go fundamentalsAI reading companion
“Optimism is an occupational hazard of programming. Feedback is the treatment.”
Read in context →The best line in the chapter, and it's borrowed. It reframes TDD as a medical regimen for a character flaw rather than a quality process — which is probably why it lands.
“We're also unnecessarily coupling ourselves to a specific implementation of the file system.”
Read in context →Notice the rhetorical sequencing: the author lists the pains of real test folders before revealing fs.FS exists. The problem is staged so the solution looks inevitable.
“func NewPostsFromFS(fileSystem fstest.MapFS) []Post”
Read in context →A test-only type in a production signature. It's deliberately wrong and he knows it — but a reader skimming for patterns could easily copy this and ship it.
8 more notes
“post := Post{Title: string(postData)[7”
Read in context →Honest ugliness. Slicing by a magic 7 would panic on any short file — he's betting that showing the embarrassing step buys more trust than hiding it.
“return nil, err //todo: needs clarification, should we totally fail if one file fails? or just ignore?”
Read in context →The most honest line of code here. A TODO left in published teaching material admits the method surfaces questions it can't answer — and then the chapter never comes back to it.
“Does newPost have to be coupled to an fs.File ? Do we use all the methods and data from this type? What do we really need?”
Read in context →This is the actual transferable skill in the chapter, not the file reading. "Ask for the narrowest thing you actually use" is the whole Go interface philosophy in one question.
“This function could never return an error.”
Read in context →He keeps an unused error return on speculation about the future — the same over-imagining of requirements he spent the intro warning against. Interface stability is the defense, but it's an exception not acknowledged as one.
“we're not doing anything interesting with the error, we're just propagating it, so it's not worth the hassle of writing a new test”
Read in context →A rare and welcome admission that TDD rigor is negotiable. Worth noting he writes the stub anyway — so the reader gets the technique without the obligation.
“what if the order of the metadata fields is different?”
Read in context →The honest confession is that readMetaLine is positional, not keyed — elegant line reading is actually a rigid parser in disguise. Every unhandled case listed here is one the fixed-sequence design made harder.
“Notice the symmetry between the production code”
Read in context →The payoff is real and cheap to miss: os.DirFS and fstest.MapFS slot into the same hole, so the test isn't a simulation of usage, it is usage.
“It's important to note that these new features only have operations for reading files.”
Read in context →Good that this is flagged, but it's buried after the wrap-up. The read-only nature of io/fs is a design constraint worth knowing before you build an abstraction on it, not after.
A reader’s takeAI reading companion
The elegant readMetaLine closure is actually a brittle positional parser
A chapter that teaches almost nothing about reading files and a great deal about sequencing work. Its real argument is that abstraction should be discovered at the refactor step rather than designed at the start, and the strongest moment is the interrogation of newPost's parameter — 'we only pass this to io.ReadAll, so ask for io.Reader' — which is the entire Go interface ethos reduced to a single habit. The honesty is unusual for teaching material: ugly magic-number slicing is shown rather than hidden, a genuine TODO about per-file error policy survives into print, and the author explicitly licenses skipping a test when propagating an error isn't interesting.
Two cracks, though. The retained error return on a function that cannot fail is the author over-imagining future requirements, the exact sin the introduction warns against, and it passes without being named as an exception. More substantially, the deferred-requirements list at the end — wrong format, non-.md files, reordered metadata — quietly exposes that the elegant readMetaLine closure is a positional parser with no tolerance for variation. The claim that 'we shouldn't have to change our design, just implementation details' holds for NewPostsFromFS's signature but is doing real work to conceal that newPost's internals would need rewriting, not extending.
Continue reading
Still, the closing symmetry between os.DirFS in main and fstest.MapFS in the test earns the whole approach: the test exercises the real consumer path, not a mock of it. The lesson to carry out is narrower than TDD evangelism — take the interface from the standard library, then take the smallest part of it you actually touch.
Read Go fundamentals with these cards beside each passage.
Open in FeedLitraGo fundamentals: HTML Templates
How does the markdown Body finally become HTML, and does html/template escape it?
How Go fundamentals unfoldsAI-assisted
How it opens. The chapter begins with an argument rather than code: not every website needs a JavaScript single-page application. The author grounds this in what Go developers tend to value — a simple, stable, fast toolchain — and notes that heavy frontend build systems deliver the opposite. If plain HTML and CSS served directly from Go will do the job, the standard library is already enough. That premise frames this chapter: the goal is to render a blog's HTML in Go, test-first.
How it develops. True to the book's method, the first green implementation is deliberately crude — a long chain of fmt.Fprintf calls with repetitive error handling. It is ugly, and the author says so plainly, admitting it won't survive as the markup grows. But it is working software, and working software is something you can learn from. The payoff comes immediately: swapping that chain for template.New("blog").Parse plus Execute is a pure refactor. The tests never change and keep passing while the code shrinks. This is the chapter's recurring rhythm — make it work crudely, then change the approach under the protection of the tests.
The first turn: how you assert. As the HTML grows, the tests themselves become the problem. Asserting on exact markup strings means re-typing the expected output by hand after every tweak, so the tests rot. The remedy is approval testing: the expected output lives in a file; the first run fails and writes a received file; you read it, copy it over the approved one, and the test goes green. The loop becomes change template → run → eyeball → approve. The author is careful to say this is still using tests as a design tool, not abandoning testing. From there the work proceeds structurally — templates move into embedded .gohtml files, top and bottom partials are composed — and then into performance-shaped design: parsing the template on every request repeats identical work, so the parse moves into a NewPostRenderer constructor, leaving each render with nothing to do but fill in data. A benchmark makes the waste visible, dropping from 53812 to 3131 ns/op. Note the framing: the author concedes that for a single post the absolute saving is usually negligible. The benchmark is evidence for a design argument, not a performance mandate.
The second turn: where logic lives. Rendering an index page introduces a new requirement — sanitising titles so they work in URLs — and with it the chapter's sharpest lesson. Wiring sanitiseTitle in through template.FuncMap works, but it traps the logic behind the template: its edge cases can only be exercised by rendering the whole page and diffing HTML. That is a poor way to test a business rule, and it is also, implicitly, a warning against over-trusting the approval tests just established — approval files are for big, ugly output you approve by eye, not for pinning down edge cases. So the logic comes out of the template. Here the simplest fix that works is a SanitisedTitle method on Post: the template returns to merely placing data, and the rule becomes directly unit-testable.
How it closes. The chapter doesn't end on that fix as a triumph; it ends on its cost. Putting a view concern on a core domain object is the first step down the road to God Objects. The author's closing position is a conditional one: a method suffices now, and a dedicated view model earns its extra type only when the domain-to-view transformation grows enough to justify it. The chapter thus closes where it began — on judgment about tooling and structure rather than on a finished artifact — with the standing principle that templates should stay logic-less, and the logic should live somewhere a test can reach it directly.
Key claims, with the lines that support them
Plenty of websites need no JavaScript single-page app; Go can serve the HTML itself.
The author argues from what Go developers actually value — a simple, stable, fast toolchain — which heavy frontend build systems fail to deliver, so plain HTML and CSS served from Go is the better fit. This opens this chapter and sets up everything that follows.
“Many websites do not need to be an SPA. HTML and CSS are fantastic ways of delivering content and you can use Go to make a website to deliver HTML.”
HTML TemplatesRead in context →
Work in tiny steps: make crude code pass first, then change the approach while refactoring.
The first green implementation is a long chain of fmt.Fprintf calls with repetitive error checks — ugly, and the author admits it won't survive as the markup grows — but it is working software you can learn from. Swapping that chain for template.New("blog").Parse plus Execute then lands as a pure refactor: the tests never change and keep passing while the code shrinks.
“In order to discover and learn as we work, we need to work in a way that gives us frequent, high-quality feedback loops, and the easiest way to do that is work in small steps.”
HTML Templates › Write enough code to make it passRead in context →
Asserting on exact markup strings makes tests a maintenance nightmare; keep the expected output in an approval file.
Once the HTML grows, every tweak forces you to painstakingly rewrite the expected string by hand. With approval files the loop becomes: change the template, run the test, look at the received output, approve it, repeat — the author notes this is still using tests as a design tool, not abandoning them.
“Maintaining this test will become a nightmare if we have to keep painstakingly updating the assertion string every time we make minor changes to the markup.”
HTML Templates › Next: Make the template "nice"Read in context →
3 more claims
Parsing templates once inside a renderer type is both tidier and far faster than re-parsing on every render.
A benchmark measures the difference directly: time per render drops from 53812 ns/op to 3131 ns/op once NewPostRenderer holds the parsed template. The author is careful to add that for a single post the absolute saving is usually negligible.
“To stop us having to re-parse the templates over and over, we'll create a type that'll hold the parsed template, and that'll have a method to do the rendering”
HTML Templates › An excuse to mess around with BenchmarkingRead in context →
Pushing logic into templates traps that logic behind rendered-HTML comparisons, which is a poor way to test business rules.
The sanitiseTitle function passed in via template.FuncMap does work, but its edge cases can only be exercised by rendering the page and diffing markup — the author's reason for preferring logic-less templates and, from here, for reaching for a view model.
“With this current design, your only means of testing this logic is by rendering HTML and comparing strings.”
HTML Templates › Passing functions into templatesRead in context →
Here the title-sanitising belongs as a method on Post; dedicated view models earn their keep only when the domain-to-view transformation grows.
Adding SanitisedTitle to Post returns the template to plain data placement and makes the logic unit-testable — the simplest fix that works. The author flags the cost too: this is view concerns creeping into a core domain object, the road to God Objects, which is exactly when a separate view model becomes worth the extra type.
“dedicated view models become more useful when the transformation between the domain object and view becomes more involved.”
HTML Templates › Create a dedicated view model type, such as PostViewModel with exactly the data we needRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Approval files replace string assertions
Hand-written expected markup must be re-typed every time the HTML changes, so tests rot. An approval test instead stores the expected output in a file: the first run fails and writes a 'received' file, you eyeball it and copy it over the approved one, and the test turns green.
Mechanism 2
Parse once, then just execute
Re-parsing the template text on every request repeats identical work. Moving the parse into a renderer type's constructor leaves each render with nothing but filling in data, which the benchmark shows cutting time per render from 53812 to 3131 ns/op.
Mechanism 3
Logic in templates escapes unit tests
A function wired in through template.FuncMap can only be reached by rendering the whole page and comparing HTML strings, so its edge cases are tested through markup. Pulling it out as a method on the data type, or into a view model, restores direct unit testing and leaves the template merely placing data.
Common misreadings
Approval tests cover the rendered output, so they are a fine place to pin down the title-sanitising edge cases too.
Approval tests are for big, ugly output you approve by eye; they remain costlier than unit tests and sensitive to markup churn. Business rules such as title sanitising should be moved out of the template — onto a method or view model — and tested directly, which is precisely why the author refuses to leave the logic in template.FuncMap.
“With this current design, your only means of testing this logic is by rendering HTML and comparing strings.”
HTML Templates › Passing functions into templatesRead in context →
The 53812→3131 ns/op benchmark means you must hoist the parse out for performance reasons.
The author concedes that for a single post the saving is usually negligible in absolute terms. The real motive is design: a renderer type that holds the parsed template stops the code repeating identical work and reads better — the benchmark just makes the waste visible.
“To stop us having to re-parse the templates over and over, we'll create a type that'll hold the parsed template, and that'll have a method to do the rendering”
HTML Templates › An excuse to mess around with BenchmarkingRead in context →
Key concepts in Go fundamentals
- html/template
- A standard-library package that merges a template with data to produce HTML that is safe against code injection.
- Where it applies: Deliberately logic-less; for non-HTML string building use text/template instead.
- Embedding
- The //go:embed directive bakes template files or a whole file system into the compiled binary.
- Where it applies: Files are fixed at compile time, so configuration-dependent templates should still be loaded from disk.
- Approval Tests
- Tests that compare output against a saved approved file instead of an inline expected string.
- Where it applies: It shifts work away from strict TDD, since the expected output is approved after the fact rather than written first.
- io.Writer
- An interface that lets the renderer write HTML to a file, an HTTP response, or a test buffer alike.
- Where it applies: It only fixes the output destination; it says nothing about how the markup itself is produced.
- Benchmark
- A Go test that measures how long a function takes per operation, exposing waste like repeated parsing.
- Where it applies: Here the saving is small in absolute terms; the author notes the real motive is tidier code.
- View model
- A type built only to carry exactly the data a template needs, so business logic stays unit-testable elsewhere.
- Where it applies: Most valuable when the domain-to-view transformation is involved; otherwise a method on the domain type is simpler.
3 more concepts
- Logic-less templates
- Templates that only place data, with no branching or computation inside them.
- Where it applies: Go lets you pass in funcs via template.FuncMap, so the constraint is a discipline, not an enforcement.
- template.HTML
- A wrapper marking a string as trusted HTML so html/template emits it verbatim instead of escaping it.
- Where it applies: Only for trusted sources; third-party HTML must be sanitised first.
- Approval test
- A test that snapshots big ugly output and asks you to approve changes rather than hand-writing expectations.
- Where it applies: Cheaper than before but still more costly than unit tests and sensitive to markup changes.
Reader’s notes on Go fundamentalsAI reading companion
“Many websites do not need to be an SPA.”
Read in context →The whole chapter's politics in one line. He earns it later by never actually needing JS — the argument is made by the code, not the slogan.
“Notice that writing this, feels awkward. Seeing all that markup in the test feels bad”
Read in context →He deliberately writes the bad test and makes you sit in the discomfort. Pain as pedagogy — the refactor lands only because you felt why it was needed.
“New lines! Who cares? Well, our test does, because it's matching on an exact string value. Should it?”
Read in context →That "Should it?" is the hinge of the whole chapter. The bug isn't in the code, it's in what the test chose to care about.
13 more notes
“Frequently people complain about the verbosity of error handling in Go, but you might find you can find better ways to write your code so it's less error-prone in the first place”
Read in context →Neat reframe: the six if err != nil blocks weren't Go's fault, they were a sign the approach was wrong. Verbosity as a design smell detector.
“But that's just silly! TDD is a method for doing work, specifically designing; but that doesn't mean we have to dogmatically use it for everything.”
Read in context →A TDD book admitting where TDD stops paying. Credibility bought cheaply and well — and note the line he draws: TDD for API shape, approvals for appearance.
“If we had stuck with a test-after, approval tests approach here we would not be answering these questions in a controlled environment. Tests give us space to think.”
Read in context →The counterweight to the previous concession, and the sharper claim: approvals confirm output, TDD forces decisions. He switches tools by which question is open.
“the effort to not do this is also pretty negligible and should tidy the code up a bit too”
Read in context →Honest ordering — the performance win is the excuse, the real payoff is structure. The 17x speedup he gets anyway is almost a joke on premature-optimization scolds.
“got "<ol><li><a href=\"/post/Hello%20World\">Hello World</a></li”
Read in context →html/template silently URL-escaped the space — context-aware escaping doing its job uninvited. The failing test is a free lesson that this package knows it's inside an href.
“This is a powerful feature, being able to send functions in to your template will allow you to do some very cool things, but, should you?”
Read in context →The whole chapter pivots on this half-sentence. He teaches the feature properly first, then asks whether to use it — that order is itself the lesson.
“your only means of testing this logic is by rendering HTML and comparing strings”
Read in context →The real argument against template logic isn't aesthetic, it's that you lose your test instruments. Logic you can only observe through rendered strings is logic you can't interrogate.
“What Mustache-influenced templating engines give you is a useful constraint”
Read in context →Reframing a limitation as a gift. And note he just showed us the escape hatch — the constraint only teaches if you know you could break it.
“it now becomes a part of the API for a core domain object. This kind of approach over time can lead to you creating God Objects”
Read in context →SanitisedTitle looks innocent, but it's the camel's nose. The domain object now knows about URLs.
“Both options are fine, in this case I am tempted to go with the first.”
Read in context →Refreshingly un-dogmatic — he names the downside, then takes the cheap option anyway, with a tripwire for when to switch. Most books would pick the pure answer and pretend there was no cost.
“But that general instinct breaks down here: gomarkdown's parser keeps internal state”
Read in context →A rare, honest footnote: the optimization reflex is usually right and here it's a bug. Worth more than the chapter's main lesson.
“Use of this type presents a security risk: the encapsulated content should come from a trusted source”
Read in context →template.HTML is a signed waiver. For a blog where you write the markdown it's fine — but this is exactly the line user-submitted content walks across unnoticed.
“People like to bemoan the complexity of modern web development but are you sure you're not just inflicting the complexity on yourself?”
Read in context →Earned polemic — he's spent the chapter demonstrating the alternative rather than just asserting it. Still, "it's your own fault" is easier to say when your example has no client-side state.
A reader’s takeAI reading companion
Show the code working, then make your case: that order
This chapter is an argument disguised as a tutorial, and the disguise is the point. It opens with a slogan — "many websites do not need to be an SPA" — and then spends the rest of its length refusing to defend it rhetorically, defending it instead by building a working blog renderer out of nothing but the standard library. By the time the closing polemic arrives ("are you sure you're not just inflicting the complexity on yourself?"), the code has already made the case; the line is a receipt, not a claim. That ordering — demonstrate, then assert — is the chapter's best structural habit, and it recurs at every scale.
The spine is a sequence of discomforts the author deliberately walks into. He writes the fmt.Fprintf version with six error checks, then reframes the verbosity not as a Go flaw but as a design smell: the error handling was loud because the approach was wrong. He writes the test that asserts on exact markup, admits it feels bad, and lets a stray newline break it — then asks the question the whole chapter hinges on: should the test care about this? The bug was never in the code; it was in what the test chose to notice. Approval tests arrive as the answer, and crucially not as a replacement for TDD but as a division of labour: TDD when the open question is API shape, approvals when the open question is appearance. He makes the concession generously ("that doesn't mean we have to dogmatically use it for everything") and then takes back exactly the right amount of ground ("tests give us space to think"). That two-step is more valuable than either half alone — it's a working heuristic for tool selection rather than a doctrine.
Continue reading
The same honesty governs the second half. Template functions are taught properly before being questioned, and the objection turns out to be instrumental rather than aesthetic: logic you can only observe through rendered strings is logic you cannot interrogate. From there, Mustache's famous limitation gets reframed as a gift — a constraint that only teaches because he first showed you the escape hatch. The alternatives (a method on the domain type, or a view model) are weighed with the downside named out loud: SanitisedTitle is the camel's nose, the moment a core object starts knowing about URLs and begins its slow drift toward God Object. He picks the cheap option anyway and leaves a tripwire for when to switch. Most books would pick the pure answer and pretend it was free.
The details earn their keep. html/template silently percent-encoding a space inside an href is a free lesson in context-aware escaping, delivered by a failing test rather than a paragraph. template.HTML is correctly flagged as a signed waiver — fine for a blog whose markdown you write yourself, and precisely the line user-submitted content crosses without anyone noticing. The parse-once refactor is sold as structure first and speed second, and then collects a 17x win anyway, which is a quiet joke at the expense of premature-optimization scolds. And the footnote about gomarkdown's stateful parser — where the usually-correct caching reflex becomes a bug — is the kind of hard-won specific that outlives the chapter's headline thesis.
What I'd tell a reader: take the method, not the manifesto. The transferable skills here are asking what your assertions actually care about, reading error-handling noise as a signal about design, and noticing when logic has migrated somewhere you can no longer test it. The thesis itself is true but tested on easy ground — a blog has no client-side state, so "you're inflicting the complexity on yourself" is cheaper to say here than it would be in an app with live interaction. Hold the conclusion loosely and the practice tightly. The reason the chapter persuades is not that server-rendered HTML always wins; it's that the author kept letting his tests ask him uncomfortable questions and kept answering honestly, including when the answer was "this tool stops helping here."
Read Go fundamentals with these cards beside each passage.
Open in FeedLitraGo fundamentals: Generics
How would you choose between an interface and a type parameter for a new helper?
How Go fundamentals unfoldsAI-assisted
Opening: a deliberately trivial starting point. The chapter begins not with a definition of generics but with a tiny, concrete need — test helpers. The author writes AssertEqual and AssertNotEqual for int, then immediately asks them to do one more thing: work on strings. The compiler refuses, and that refusal is the real opening move. Rather than treating the error as an obstacle, the text pauses to defend it, listing what type constraints buy you: simpler implementations (you can't add a Person to a BankAccount), and protection from passing the wrong data by accident. This framing matters for everything that follows, because it establishes that the goal is not to escape the type system but to get flexibility inside it.
Development: the interface{} detour, taken seriously and then dismantled. The obvious pre-generics answer is interface{}, and the chapter actually goes there — change the signatures, watch the tests pass, note that popular assertion libraries do exactly this. Then it probes the cost. AssertEqual(1, "1") compiles; the compiler has nothing left to say about your arguments; developers end up reaching for reflection. The problem is characterised precisely: errors migrate from compile time to runtime, where they reach users. Only after this cost is made concrete does the chapter introduce the solution — [T comparable] — and it explains the syntax in terms of the constraint rather than the mechanism: comparable is chosen because the function needs == and !=, and swapping in any produces an honest compiler error. A short aside answers the natural objection ("isn't T any just interface{}?") with a clean distinction: interface{} lets you mix apples and oranges, while T binds all arguments to one type, so GenericFoo(apple1, orange1) fails.
The turn: the same argument replayed at the level of data structures. Having settled functions, the chapter restages the entire sequence for types — and this repetition is the structural heart of the piece. We get StackOfInts and StackOfStrings, near-identical twins with near-identical tests. The interface{}-based unification is tried first, again in good faith, and again it passes the tests. The decisive blow is a new test: pop two values and add them. operator + not defined on firstNum — and the fix, a type assertion with an ok check for every single value, is shown in full so the ugliness is felt rather than asserted. Stack[T any] then lands as relief rather than novelty, with Pop() (T, bool) returning a usable int, the string-stack test deleted as redundant, and a constructor NewStack[T]() added to cover the case where inference can't do the work and you must name the type explicitly.
Closing: from syntax to judgement. The final section steps back and spends its energy on culture rather than code, pre-empting the community's anxieties in turn. Will generics turn Go into Java? No — and the retort is that "use them carefully" is true of every language feature, including interfaces, which are themselves just another way of describing constraints. You already use generics, the author notes, every time you use a slice or a map. Abstraction isn't a dirty word; a codebase with none is also bad. The genuinely cautionary note is reserved for last and is methodological: don't generalise too early. The chapter points back at its own structure as evidence — it reached Stack[T] only by first building concrete, tested StackOfInts and StackOfStrings and noticing the pattern. The closing TDD sketch ("a little duplication is better than coupling to a bad abstraction… sleep on it") makes the thesis explicit: generics are a tool for consolidating behaviour you already understand, not for imagining it up front.
Key claims, with the lines that support them
Writing in a statically typed language is mostly the work of stating constraints, and doing it well makes code both safer and easier to write.
The author argues that telling the compiler which types you handle shrinks the set of valid implementations: you simply cannot add a person to a bank account or capitalise a number, so fewer wrong programs are even expressible, and accidental misuse of a function is caught for you.
“Using statically typed languages is an act of describing constraints.”
Generics › Generics are simpler than using interface{} in most casesRead in context →
Reaching for interface{} to gain flexibility throws away those constraints and shifts failures to runtime.
This is the flip side of the previous point. A stack backed by a slice of interface{} compiles happily while letting apples be pushed onto a stack of oranges; and comparing an integer with the string "1" compiles too, failing only at run time with an unhelpful message.
“Writing functions that take interface{} can be extremely challenging and bug-prone because we've lost our constraints, and we have no information at compile time as to what kinds of data we're dealing with.”
Generics › The problem with interface{}Read in context →
A generic function with T any is not merely interface{} in new clothing.
The author concedes any is just an alias for interface{} as a constraint, then draws the distinction: the interface version accepts any mixture of arguments, while the generic version ties all arguments to one type, so comparing two apples compiles but an apple against an orange does not.
“However GenericFoo still offers some constraints because we've said that it only works with one type, T.”
Generics › Is a generic function with T any the same as interface{} ?Read in context →
3 more claims
Generic data structures remove duplicated logic without the pain that interface{} causes callers.
Identical int and string stack types, each with its own tests, collapse into one Stack[T]. With interface{} a test that pops two values and adds them refuses to compile and needs an assertion plus an ok-check per value; with Stack[int] the popped values are plain ints and the addition just works, and the duplicate string-stack test can be deleted.
“However GenericFoo still offers some constraints because we've said that it only works with one type, T.”
Generics › Is a generic function with T any the same as interface{} ?Read in context →
Only generalise once concrete, tested behaviour shows a pattern worth capturing.
The author recounts his own path: concrete StackOfStrings and StackOfInts backed by tests first, then refactoring to Stack[T] once the repetition was visible. He offers the familiar "wait until you see it three times" as a rough guide, notes a little duplication beats coupling to a bad abstraction, and stresses that passing tests are what make the later refactor safe — there are no firm rules here.
“There's no hard and fast rules here but resist making things generic until you can see that you have a useful generalisation.”
Generics › Make it work, make it right, make it fastRead in context →
Generics are not a uniquely dangerous feature that will turn Go into Java.
The author answers the community's fear by pointing out that interfaces are also a way of describing constraints, and a badly chosen interface is just as confusing — so "use carefully" is advice that applies to every language feature. He adds that anyone using slices and maps has already been consuming generic code, since you cannot append an orange to a slice of apples, and that a codebase with no abstraction at all is no better.
“Using statically typed languages is an act of describing constraints.”
Generics › Generics are simpler than using interface{} in most casesRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
How interface{} erases checks
Declaring parameters as interface{} tells the compiler nothing about the data, so it cannot know which operators are legal. Checks that could have failed at build time move to runtime, forcing type assertions or reflection and allowing mismatched values through.
Mechanism 2
One label, one type
A type parameter such as T is a single placeholder bound once per call, so every argument declared T must share that one type. Because the type is usually inferred from the arguments, callers get flexibility across types while each individual call stays type-checked.
Mechanism 3
Typed struct, typed methods
Putting the type parameter on the struct makes the methods take and return that same type, as if a separate implementation were generated for each instantiation. So Stack[int].Pop hands back an int, and the duplicated per-type structs collapse into one.
Common misreadings
Since any is literally an alias for interface{}, writing a generic function with T any buys nothing over just taking interface{}.
The two differ in where the type is fixed. A type parameter binds all the arguments (and the return) to one type chosen per call, so mixed-type calls such as passing "1" and 1 fail to compile and the caller gets the real type back without an assertion; an interface{} parameter accepts any combination and returns something the compiler knows nothing about.
“However GenericFoo still offers some constraints because we've said that it only works with one type, T.”
Generics › Is a generic function with T any the same as interface{} ?Read in context →
Generics beat duplication, so as soon as two similar types appear it is time to make them generic.
This chapter advises waiting for a generalisation you can actually see. The author deliberately built concrete stacks with tests first, cites the rule of thumb of waiting for the third occurrence, and warns that a bit of duplication is preferable to coupling yourself to an abstraction invented before you understood the behaviour.
“There's no hard and fast rules here but resist making things generic until you can see that you have a useful generalisation.”
Generics › Make it work, make it right, make it fastRead in context →
Key concepts in Go fundamentals
- Type parameter
- A labelled placeholder type, like T, declared in square brackets so a function or struct can work with one caller-chosen type.
- Where it applies: Often inferred from arguments; must be written explicitly when it cannot be inferred, as with a constructor.
- comparable constraint
- A constraint saying the type supports == and !=, which is what assert helpers need.
- Where it applies: Using any instead fails to compile because those operators are not defined for every type.
- Generic data structure
- A struct declared with a type parameter, so Stack[int] stores and returns ints only.
- Where it applies: Syntax mirrors generic functions; the stack's type fixes what its methods accept and return.
- Interface
- Here used as interface{} / any, meaning "anything", which gives flexibility but erases type information.
- Where it applies: any was added in 1.18 as an alias for interface{}; unlike a type parameter it allows mixing types in one call.
0 more concepts
Reader’s notes on Go fundamentalsAI reading companion
“In software, constraints are often extremely helpful.”
Read in context →The real thesis of the chapter, buried in an aside. Types aren't paperwork, they shrink the space of wrong programs you can even write.
“although the error message got 1, want 1 is unclear”
Read in context →Nice honest detail: the failure does fire, it just lies about why. That's worse than not compiling, and it's the whole argument against interface{} in one line.
“any was added in 1.18 and is _just an alias for interface”
Read in context →So the constraint isn't in the word any at all — it's in the single label T forcing both arguments to agree. The power is in the binding, not the bound.
5 more notes
“For the sake of brevity I've omitted the TDD process”
Read in context →A TDD book skipping TDD. Understandable, but it quietly undercuts the later moral about arriving at abstractions through cycles — we're handed the duplication pre-cooked.
“If you do this, our tests still pass. Who needs generics?”
Read in context →Best rhetorical beat in the chapter: let the bad version pass the suite, then show the suite was never the point. Type safety is about the code you can't write, which tests can't measure.
“a little duplication is better than coupling to a bad abstraction”
Read in context →The honest brake on everything above. Also notice: the chapter advertises generics, then spends its last pages telling you to wait before using them.
“Thank goodness I am so smart and good-looking because I use TDD”
Read in context →The joke is charming but it's doing persuasive work — self-deprecation smuggling in a fairly strong claim that TDD is what licenses fearless refactoring.
“you've already been a consumer of generic code”
Read in context →Strongest disarming argument against the FUD: slices and maps were always parametrically typed, just with the privilege reserved for the compiler authors.
A reader’s takeAI reading companion
Brilliant generics chapter, but the TDD justification for it is missing
A teaching chapter that works mostly by staging a failure it then refuses to accept. The pedagogical spine is strong: int helper → string breaks → interface{} 'fixes' it → the fix rots at the call site (operator + not defined) → generics. The sharpest move is letting the interface{} Stack pass the full test suite and then asking 'who needs generics?' — because the answer isn't in the tests, it's in the programs the compiler would have stopped you from writing, and that's precisely what a green suite cannot show. The clearest conceptual payoff is the GenericFoo/InterfaceyFoo comparison, which reveals that the constraint never lived in the word any (an alias for interface{}) but in the shared label T forcing both parameters into agreement — relational typing, not restrictive typing.
Two soft spots. First, the chapter omits the TDD process that produced the duplicated stacks, in a book about TDD, right before arguing that only TDD cycles earn you the right to generalize; the duplication is handed over pre-made, so we see the refactoring conclusion without the evidence that justified it. Second, the final third reverses the sales pitch — rule of three, sleep on it, prefer duplication to a bad abstraction — which is wise but leaves the reader holding a tool they've just been told to postpone using. The defensive sections ('Will generics turn Go into Java? No.') are the most political writing here, and the best line in them is the quietest: if you've used a slice or a map you've already been consuming generics, previously a privilege reserved for the language's own authors.
Continue reading
Underneath it all sits the line I'd keep: static typing as the act of describing constraints, with a smaller solution space as the reward rather than the cost.
Read Go fundamentals with these cards beside each passage.
Open in FeedLitraGo fundamentals: Revisiting arrays and slices with generics
How would Map and Filter be written with the same two-type-parameter trick?
How Go fundamentals unfoldsAI-assisted
The chapter opens by looking back rather than forward. It reprints Sum and SumAllTails from the earlier arrays-and-slices chapter and asks the reader to spot the shared shape in them: establish an initial result, walk the collection applying a combining function, return the result. That shape, the author notes, has a name in functional programming — fold, or reduce — and he quotes an encyclopedic definition to make the point that this is a well-established family of higher-order functions, not an invention of his. Go has had higher-order functions all along and, since 1.18, generics too, so these patterns are now expressible. Anticipating resistance ("Go is supposed to be simple"), he answers with two distinctions he leans on throughout: easy is not the same as simple (gesturing at Rich Hickey's talk), and unfamiliar is not the same as complex.
From there the chapter becomes a disciplined exercise in refactoring. The author warns against the usual mistake with a new language feature — reaching for it without a concrete use case — and frames generics as a tool for the refactor step of TDD, where existing tests act as a safety net and ideas can be tried and reverted freely. He invites the reader to attempt Reduce first, offering hints about its arguments (the collection, a combining function, and — nudged by the fold literature — an initial value), then shows a first pass: a generic function over one type parameter that folds a slice with func(A, A) A. Sum and SumAllTails are rewritten so that each declares its combining logic on its first line and delegates the iteration to Reduce. Small tests for multiplication and string concatenation show the reusability, and the multiplication case motivates a short digression on why the initial value must be supplied: starting from zero would annihilate a product, so one is the right seed — the identity element.
The turn comes when the pattern is stretched past its first design. Reducing a list of Transaction values into a float64 bank balance won't compile, because the single type parameter forces input and output to match. The fix is a one-line signature change — two type parameters and func(B, A) B — leaving the body and every existing caller untouched, which the author presents as evidence that the abstraction was loosened rather than broken, and that it remains type-safe. He reaches the balance function the honest way first: a plain loop, written and committed while the tests pass, and only then rewritten through Reduce. An "extending the bank" section pushes the ergonomics further, with Account, NewTransaction, and NewBalanceFor folding transactions over an account via applyTransaction — code he argues reads declaratively, saying what happens rather than how, with mutation and looping quarantined inside Reduce.
The closing stretch generalizes and then moralizes. Fold, he says, is universal — mixing colors, counting votes, totalling a basket, nearly anything that processes a list — and Find follows as a second, easier example: a generic predicate search returning a value and a found flag, reused on both integers and a small Person slice (with a joke at his own expense about the result being "flawless"). The wrap-up restates the rule of thumb that drove this chapter: let TDD surface real behaviour you need, and discover abstractions in the refactor step, committing on green so a mess is always recoverable. Two final notes round it off — use the established names (a collection of A turned into B is Map, not Convert), and treat "this isn't idiomatic" as an empty objection unless it comes with reasons, since idioms must shift as the language does. Links to fold resources close the chapter.
Key claims, with the lines that support them
The hand-written Sum and SumAllTails are two instances of one pattern that already has an established name outside Go: fold, or reduce.
The author points out that both functions seed a result, combine each item into it, and return it, then shows the payoff by rewriting them as Reduce(numbers, add, 0) and Reduce(numbers, sumTail, []int{}) — after which each function's body states only its own computation and the iteration disappears into Reduce.
“This idea is commonly talked about in functional programming circles, often times called 'reduce' or fold)”
Revisiting arrays and slices with genericsRead in context →
Objecting that Go should stay simple confuses easy with simple, and unfamiliar with complex.
The author's reasoning is that writing loops and copy-pasting them is easy yet not simple, while fold only sounds academic: stripped of jargon it is just taking a collection and combining it into one item, something readers already do constantly. Using the recognised name also lets programmers coming from other languages read the intent immediately.
“Don't conflate easiness, with simplicity. Doing loops and copy-pasting code is easy, but it's not necessarily simple.”
Revisiting arrays and slices with genericsRead in context →
Generics pay off when they come out of the refactoring step on already-tested code, not when a shiny feature goes looking for a use.
Building on the previous point, the author first writes the bank balance with an ordinary loop, commits it while the tests pass, and only then experiments with a Reduce version — when the experiment fails to compile, the cost is a type-signature change rather than broken software. The argument is that passing tests plus committed work make the experiment revertible, which is what lets abstractions be discovered instead of guessed; the author concedes the loop version was not bad code and the rewrite is partly for demonstration.
“We've added a second type constraint which has allowed us to loosen the constraints on Reduce.”
Revisiting arrays and slices with generics › RefactorRead in context →
3 more claims
Reduce stays useful once the result type differs from the element type, and widening it costs almost nothing.
Computing a balance from a slice of transactions means folding Transaction values into a float64, which the one-parameter version rejects with a type-mismatch error. Adding the second parameter fixes it with no change to the body or to earlier callers, and the same trick lets NewBalanceFor fold transactions into an Account, keeping the function type-safe.
“We've added a second type constraint which has allowed us to loosen the constraints on Reduce.”
Revisiting arrays and slices with generics › RefactorRead in context →
Code written this way reads as a description of what happens rather than how it happens.
NewBalanceFor is just Reduce applied to the transactions with applyTransaction and a starting account, so a reader skimming files sees the intent at once and can drop into applyTransaction for the business rules without tracing loops or mutated state. The author links this to the way we usually read code — darting between files for the what, not the how.
“This idea is commonly talked about in functional programming circles, often times called 'reduce' or fold)”
Revisiting arrays and slices with genericsRead in context →
Borrow the names these patterns already carry instead of inventing your own.
The author's example: a function turning a collection of A into B is Map, so calling it Convert hides it. Established names lower the mental cost for other readers and make the idea searchable, which is the same reason using the word Reduce communicated the intent in the first place.
“Don't conflate easiness, with simplicity. Doing loops and copy-pasting code is easy, but it's not necessarily simple.”
Revisiting arrays and slices with genericsRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
How a fold collapses a list
Reduce holds one accumulator, starts it at a supplied initial value, and rewrites it by applying a combining function to the accumulator and each item in turn; after the last item the accumulator is the answer. Because the looping and the state-updating live inside Reduce, each caller only has to supply the combining step.
Mechanism 2
Why the seed must be the identity
The initial value is fed straight into the first combination, so it must be the operation's neutral element or it distorts the result. Addition's neutral element is 0, which matches Go's zero value by luck; multiplication's is 1, so leaning on the zero value would multiply everything by 0 and always return 0.
Mechanism 3
Second type parameter widens Reduce
With one type parameter the combiner is inferred as func(A, A) A, so a bank balance accumulator of a different type than the collection's elements fails to compile. Declaring Reduce[A, B any] with f func(B, A) B separates accumulator type from element type; the loop body and every existing caller stay untouched because the old uses are just the case where A and B coincide.
Common misreadings
Saying these higher-order functions "aren't idiomatic Go" is enough reason to reject them.
The author treats that phrase, offered without detail, as neither actionable nor useful — especially about features the language only just gained. Idioms shift as the language changes, so patterns deserve to be argued on their merits, and well-designed tests mean a team can always refactor back if a style turns out not to suit them.
“Don't conflate easiness, with simplicity. Doing loops and copy-pasting code is easy, but it's not necessarily simple.”
Revisiting arrays and slices with genericsRead in context →
Now that generics exist, the move is to build a library of generic helpers like Reduce, Find and Map up front and use them everywhere.
The chapter's order is the opposite: drive out real, specific behaviour with TDD, commit it while green, and only then look for an abstraction that tidies it up. Reaching for a new feature without a concrete use case leaves you guessing, which is how premature abstractions get in — and the author still notes that the plain-loop balance function was perfectly acceptable code.
“We've added a second type constraint which has allowed us to loosen the constraints on Reduce.”
Revisiting arrays and slices with generics › RefactorRead in context →
Key concepts in Go fundamentals
- Reduce (fold)
- A higher-order function that walks a collection and combines each item into a single accumulated result, starting from an initial value.
- Where it applies: Fits anything that collapses a list into one value; it hides the loop, so the combining function must carry all the real logic.
- Identity Element
- The starting value that leaves every element unchanged under the operation: 0 for addition, 1 for multiplication.
- Where it applies: Depends on the operation, so Go's default zero value is not always the right initial value.
- Find
- A generic function that returns the first item in a collection matching a predicate, plus whether one was found.
- Where it applies: Only returns the first match; needs a predicate per use case.
- Refactoring
- The stage after tests pass where you reshape working code, protected by the test suite.
- Where it applies: Only safe once tests pass and work is committed.
0 more concepts
Reader’s notes on Go fundamentalsAI reading companion
“Don't conflate easiness, with simplicity.”
Read in context →The strongest line in the chapter, and borrowed. But it cuts both ways — Hickey's point would also condemn a Reduce that hides three concerns behind one name.
“A mistake people often make with shiny new language features is they start by using them without having a concrete use-case.”
Read in context →The author's real thesis, and better than the generics material around it. Refactoring as the only safe place to introduce abstraction is a claim worth more than one paragraph.
“return Reduce(numbers, sumTail, []int”
Read in context →This is the weakest example in the chapter. The accumulator appends — it's building a list, not folding to a value. That's Map wearing Reduce's coat, and the author later scolds people for exactly this kind of misnaming.
5 more notes
“If you wish to sound clever with your nerd friends, you'd call this The Identity Element.”
Read in context →The joke undersells it. Identity isn't a party trick — it's why the initial value argument is non-optional in the signature, and the chapter almost makes that connection.
“We won't have to change the function body, and we won't have to change any of our existing callers.”
Read in context →Genuinely elegant — loosening a type constraint is source-compatible here only because inference still resolves the old calls. Worth noticing that this is a property of Go's inference, not a general law.
“func applyTransaction(a Account, transaction Transaction) Account”
Read in context →Quietly relies on Go's value semantics: a is a copy, so mutating Balance is safe. Swap Account for a pointer and this 'declarative' fold becomes a shared-state bug. The purity is accidental.
“As you can see, this code is flawless.”
Read in context →The one moment the author winks. Find returns the zero value on miss, which for Person is an empty struct indistinguishable from a real empty name — the found bool is load-bearing and easy to ignore.
“Without any more detail, is not an actionable, or useful thing to say”
Read in context →Fair, but it's a rebuttal to the weakest version of the objection. The serious idiom argument is about team legibility and debuggability, not dogma, and the chapter doesn't meet it.
A reader’s takeAI reading companion
Test first, abstract later, the lesson that actually transfers
A chapter doing two jobs with uneven success. As a TDD argument it's excellent: commit the working loop first, then experiment with abstraction under test cover, revert if it doesn't earn its keep. That sequencing is the real lesson and it's genuinely transferable. As a generics tutorial it's shakier — the SumAllTails fold is really a map, the Find example's zero-value return gets waved past with a joke, and the Account fold's apparent purity rests on Go value semantics the text never names. The defensive closing sections are the tell: the author knows a Go audience will push back, and chooses to pre-empt rather than engage. 'Don't conflate unfamiliarity with complexity' is true, and also exactly what someone says when they'd rather not test whether the complexity is real. The best advice in the whole piece is the smallest: go learn what the pattern is already called before you name it yourself.
Read Go fundamentals with these cards beside each passage.
Open in FeedLitraTesting fundamentals: Introduction to acceptance testing
How do acceptance tests act as a north-star when written before the code?
How Testing fundamentals unfoldsAI-assisted
Opening: a workplace problem, not an abstraction. The chapter begins with a concrete pain point from the author's job — services running on Kubernetes needing "graceful shutdown" — and anchors it with a homely analogy about finishing a phone call rather than hanging up mid-sentence. From there it supplies just enough Kubernetes background to make the stakes legible: frequent deploys (justified by DORA metrics) mean pods are terminated often, and termination begins with a SIGTERM that is effectively a warning before an unappealable SIGKILL. The author then shows what ignoring that warning costs: in-flight HTTP requests die, automated API tests fail intermittently, an alert fires, and a developer has to drop their work to investigate — and real users would see 5xx errors instead. Having established the cost, the chapter states the desired behaviour as three steps (stop accepting requests, let in-flight ones finish, then exit) and notes that Go's standard library already supplies the pieces: Server.Shutdown plus signal.Notify. A small main function demonstrates the author's own gracefulshutdown package wrapping an http.Server in the decorator pattern.
The pivot: unit tests weren't enough. The chapter's real turn comes when the author admits that although the package had unit tests sufficient to refactor confidently, they still didn't feel confident it worked. Their substitute was manual testing — start a real program, send a request, fire a SIGTERM, watch — which the author characterizes as boring, unscalable, imprecise and wasteful. That discomfort motivates the chapter's actual subject. Acceptance tests are then defined as black-box or functional tests that exercise the system only through its public interface, as a user would, and the author connects this to the familiar Go habit of putting tests in mypkg_test rather than mypkg. A balanced ledger follows: the benefits (whole-system confidence, cheaper and more accurate than manual checks, documentation that can't drift, no mocks) set against the drawbacks (expensive to write, slow, design-coupled, hard to debug, silent about internal quality, and unable to reach every scenario). The conclusion drawn is deliberately moderate — relying on acceptance tests alone is foolish; they belong in a test pyramid alongside unit tests, with lead time as the metric that punishes imbalance.
Development: showing the machinery. The chapter then returns to the graceful-shutdown example and turns the manual ritual into an automated one, listing the steps plainly: build the binary, run it and wait for it to listen, send a request, send SIGTERM before the response can arrive, and check a response still comes back. The implementation is given as an acceptancetests package whose exported LaunchTestProgram builds a randomly-named binary, starts it, polls the port until something answers, and hands back two closures — a cleanup function that kills the process and deletes the binary, and an interrupt function that sends SIGTERM. The author is candid that this code isn't beautiful and tells the reader to ignore the unexported boilerplate; the payoff is that the setup cost is paid once. The resulting test reads almost as prose: confirm the server works, schedule a SIGTERM 50ms after firing a request, assert the request still succeeds, then assert further requests fail. Helpers like assert.CanGet (a goroutine-and-select timeout wrapper) keep the suite DRY, and the author stresses that none of this needs a framework — the standard library suffices. A pasted go test output showing two parallel example programs, one graceful and one not, serves as the small-investment-big-payoff proof.
Closing: generalization and a forward look. The wrap-up lifts the lesson off the specific example: the mechanics of an acceptance test depend on what you're building — a headless browser like Selenium for a website, an HTTP client for a REST API — but the black-box principle is constant, and non-trivial systems with databases will need something like docker-compose to stand up a local environment. The chapter then flags its own limitation: here the acceptance test was written after the code, whereas (citing Growing Object-Oriented Software) such tests can serve as a "north star" driving design — the subject of the next chapter, along with techniques for stopping acceptance-suite costs from spiraling, a failure mode the author says has hamstrung many teams. It ends on two softer notes: an appeal to open-source authors to ship runnable example programs with readable acceptance tests as a trust- and maintenance-saving investment, and a brief recruiting pitch for the author's employer.
Key claims, with the lines that support them
Unit tests alone did not make the authors confident the shutdown package worked, and the manual checks they fell back on should have been automated instead.
The author describes the real failure that motivated the work: a pod terminating mid-request killed the server, an automated API test got no response, and the resulting alert pulled a developer off their work — with users hitting 5xx in the same situation. Unit tests passed, so the team resorted to starting a demo program by hand, sending a request, and firing SIGTERM themselves; the author calls that boring, unscalable, imprecise and wasteful, which is the argument for turning exactly those steps into a test.
“The engineer in you should be feeling uncomfortable with manual testing. It's boring, it doesn't scale, it's inaccurate, and it's wasteful.”
Introduction to acceptance testing › Tests and feedback loopsRead in context →
An acceptance test should drive the whole running system from outside, as a user would.
Following from the previous point, the author spells out the recipe his test performs: build the binary, run it, wait until something is listening on the port, issue a GET, fire SIGTERM before the handler can reply, and check a response still arrives — then check that further requests are refused. Nothing is mocked and nothing reaches inside the package, so the assertions describe behaviour a caller could observe; the slow handler exists only to open a window in which the signal can land mid-flight.
“The engineer in you should be feeling uncomfortable with manual testing. It's boring, it doesn't scale, it's inaccurate, and it's wasteful.”
Introduction to acceptance testing › Tests and feedback loopsRead in context →
Go's standard library is already enough to write acceptance tests; no dedicated framework is needed.
The whole harness is one helper, LaunchTestProgram, built from ordinary packages: os/exec compiles and starts the binary, net dials the port in a retry loop until the server answers, syscall sends SIGTERM, and the helper hands the test back a cleanup function that kills the process and deletes the temporary binary. The author concedes this plumbing is not pretty, but it is written once and then ignored.
“It's important to note again, Go has all the tools you need to write acceptance tests out of the box. You don't need a special framework to build acceptance tests.”
Introduction to acceptance testing › The acceptance test(s)Read in context →
3 more claims
Setting up acceptance tests costs more up front, but the payoff is readable tests and fast, trustworthy feedback.
Once the ceremony is hidden behind the launcher and small helpers like CanGet/CantGet — which run the request on a goroutine and fail if nothing comes back within three seconds — the test body is a short, legible description of the behaviour, and it was easy to write a second one for the version without graceful shutdown so readers can compare. The author shows the full suite finishing in a few seconds per package, versus a human repeating the ritual by hand.
“The engineer in you should be feeling uncomfortable with manual testing. It's boring, it doesn't scale, it's inaccurate, and it's wasteful.”
Introduction to acceptance testing › Tests and feedback loopsRead in context →
Leaning only on acceptance tests is a mistake; they belong on top of a base of unit tests.
Turning the earlier enthusiasm around, the author lists what these tests cannot do: they are expensive to write, slow to run, coupled to the system's design, rarely show a root cause when they fail, say nothing about internal quality, and cannot reach some scenarios at all because they stay outside the box. Unit tests are what enable fearless refactoring and fast feedback, so a sensible mix is described by the test pyramid — which also protects the sub-ten-minute lead time the team targets.
“For this reason, it is foolish to only rely on acceptance tests. They do not have many of the qualities unit tests have, and a system with a large number of acceptance tests will tend to suffer in terms of maintenance costs and poor lead time.”
Introduction to acceptance testing › Potential drawbacks vs unit testsRead in context →
Authors of shared packages should ship small example programs covered by acceptance tests.
The author's reasoning is about trust: with such tests in place, readers can look at the example program and know the example genuinely works, so the package's claims are credible. He compares the effect to testable examples — a little extra developer-experience effort that buys confidence and, because the examples are verified rather than drifting from reality, lowers the maintainer's own support burden.
“The engineer in you should be feeling uncomfortable with manual testing. It's boring, it doesn't scale, it's inaccurate, and it's wasteful.”
Introduction to acceptance testing › Tests and feedback loopsRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Why the grace period saves in-flight requests
Kubernetes first sends SIGTERM and only later SIGKILL. If the program catches that signal and asks the HTTP server to shut down, the server closes its listeners so no new connections arrive, lets the requests already being served write their responses, and only then exits — so callers get real answers instead of a dropped connection.
Mechanism 2
Why black-box tests can't cheat
Because the test only touches the program's public interface and has no handle on its internals, it cannot reach in and arrange a convenient state to make itself pass. Whatever it asserts has to be observable from outside, the same way a real user would see it, so a green test actually reflects end-to-end behaviour.
Mechanism 3
How heavy acceptance suites slow delivery
Each acceptance test runs a whole real system, so it is slow to execute and costly to write and keep working; failures point at the system rather than a root cause, so debugging eats time. Pile up enough of them and every merge waits on a long, fragile suite, pushing lead time out.
Common misreadings
If the acceptance tests are green, the code inside must be in decent shape.
These tests only judge externally visible behaviour, and the author says plainly that you could write total garbage and still make one pass. Judgements about internal quality come from unit tests, which are also what make refactoring safe — which is why he keeps both rather than replacing one with the other.
“For this reason, it is foolish to only rely on acceptance tests. They do not have many of the qualities unit tests have, and a system with a large number of acceptance tests will tend to suffer in terms of maintenance costs and poor lead time.”
Introduction to acceptance testing › Potential drawbacks vs unit testsRead in context →
With graceful shutdown wired up, the server can take as long as it needs to finish whatever it is doing.
The window is bounded twice over: Kubernetes follows SIGTERM with SIGKILL after a fixed grace period, and the shutdown call itself gives up and returns an error if its context deadline passes first. That is why the example program treats an error from shutdown as fatal and warns that some responses may have been lost — handlers still have to be fast enough to fit in the window.
“Graceful shutdown makes sure your system finishes its work properly before it is terminated.”
Introduction to acceptance testingRead in context →
Key concepts in Testing fundamentals
- Acceptance test
- A black-box test that drives the whole running system through its public interface, like a real user.
- Where it applies: Cannot reach internals, so some scenarios are impractical and failures rarely show a root cause.
- Graceful shutdown
- On SIGTERM, stop accepting new requests, let in-flight ones finish, then exit.
- Where it applies: Bounded by the grace period before SIGKILL and by the shutdown context deadline.
- Lead time
- Time from merging a commit to it running in production.
- Where it applies: Bloated acceptance suites inflate it; the chapter's target is under 10 minutes.
0 more concepts
Reader’s notes on Testing fundamentalsAI reading companion
“A real-world analogy would be someone trying to wrap up a phone call properly”
Read in context →Warm analogy, but it hides the hard part: the grace period is a deadline. The phone call gets hung up anyway if you ramble past SIGKILL.
“The engineer in you should be feeling uncomfortable with manual testing.”
Read in context →Nice rhetorical move — shame as motivation. It works because he already confessed to doing the manual thing himself first.
“it can't use any special workarounds that could make a test pass”
Read in context →The real argument for black-box: it removes your own ability to cheat. Tests aren't just checking the code, they're checking the author.
6 more notes
“You could write total garbage and still make an acceptance test pass.”
Read in context →The most honest line in the chapter. Acceptance tests certify behaviour, never design — which is exactly why TDD people guard the pyramid so jealously.
“Admittedly, this is not the nicest code in the world”
Read in context →And that's the actual lesson buried in an aside: test harness code gets a different quality budget because it's write-once, read-never.
“Without graceful shutdown, this would fail”
Read in context →A comment doing the work of two tests. The pairing of with/without programs is the real design idea here — the contrast is the assertion.
“Go has all the tools you need to write acceptance tests out of the box”
Read in context →True, and quietly polemical — aimed at the framework-industrial complex. Though exec.Command plus random binary names is a framework, just an unnamed one you now maintain.
“the costs of writing and maintaining acceptance tests can quickly spiral”
Read in context →Good that he says it, but 4.8 seconds for one shutdown test already hints at the slope. Multiply by a hundred scenarios and the sub-10-minute lead time is gone.
“please reach out to me on Twitter, and maybe we can work together”
Read in context →The DORA metrics and the $WORK framing suddenly read differently — the chapter is partly an engineering-culture advert. Doesn't invalidate it, but worth naming.
A reader’s takeAI reading companion
Unit tests passed but he still doubted his code
A short, practical chapter that earns its authority by admitting weakness: the unit tests passed and the author still didn't believe his own library, so he automated the embarrassing manual ritual he'd been performing by hand. The strongest material is the honest ledger of drawbacks — especially the concession that an acceptance test can be green over garbage — which reframes acceptance tests as confidence instruments rather than quality instruments, and makes the pyramid argument feel derived rather than recited. Two things go under-pressed. First, the anti-framework claim: Go's stdlib does suffice, but LaunchTestProgram is a bespoke harness with build, port-polling, signal and cleanup concerns, i.
e. the thing he says you don't need, minus the community maintaining it. Second, the cost curve: a single graceful-shutdown test costs ~5 seconds of wall clock, and the chapter's own 10-minute lead-time target is a hard budget that this style of test spends fast — acknowledged, but deferred to the next chapter. The with/without program pairing is the most transferable idea on the page: a behaviour claim is only legible next to its negation. And the $WORK framing plus closing recruitment pitch is a reminder that the DORA vocabulary here is both a method and a brand.
Read Testing fundamentals with these cards beside each passage.
Open in FeedLitraTesting fundamentals: Scaling Acceptance Tests (and light intro to gRPC)
How does the specification stay stable once gRPC becomes a second driver?
How Testing fundamentals unfoldsAI-assisted
The opening: a diagnosis. The chapter begins not with code but with a complaint about acceptance tests — that they are expensive to maintain — and then reframes where the expense actually comes from. The usual suspect is abstraction, but the author points instead at coupling to implementation detail. The anchoring example is a Selenium test that breaks when one HTML tag is swapped for another: nothing a user could perceive has changed, yet the test must be repaired, because the test knows about internals it has no business knowing. From that diagnosis follows the chapter's organising proposal: split an acceptance test into three layers — the specification (what behaviour is wanted), the driver (how to talk to the system), and the system itself. Only the driver knows about markup, URLs, and protocols, so churn lands there and the specification stays still unless the behaviour genuinely changes. The author extends the point one step further: a specification clean of implementation detail can be run by different drivers against different environments — through a headless browser, or by direct HTTP calls against the same API — even against production, with the honest caveat that a green run in staging still proves nothing about production. The opening closes by setting up the vehicle for the rest of the chapter: a Go "greeter," built top-down, with Docker and Testcontainers so the test exercises a real running system.
The development: changes that start at the spec and push outward. Having argued for the layering, the chapter puts it under load. The author adds a name parameter — the greeter should greet someone — and the discipline is to edit the specification first. That edit immediately breaks compilation of the driver, and fixing the driver breaks the server; each compiler error points at the next thing to change, so the work is pulled along by the desired behaviour rather than guessed at from the bottom up. This is also where the chapter pays its most honest price. Starting from an acceptance test means buying a specification, an HTTP driver, a Dockerfile, and a container-based test before the first real feature exists. The justification is reported experience rather than measurement: engineers who build a chunk of code in isolation often find it does the wrong thing, carries work nobody asked for, resists integration, and gets rewritten — whereas a failing acceptance test states the goal before any code is written.
The turn: a new protocol is only a new adapter. The chapter's pivot is a restructuring — domain code, adapters, and cmd pulled apart — followed by the test of whether the layering was real. The author's stance is that transports like HTTP and gRPC are complexity that exists only because computers are involved; they are not part of the domain. So the greeting logic lives in one core domain function, and the protocol lives in an adapter. The payoff is the chapter's strongest piece of evidence: a brand-new gRPC greeter differs from the HTTP one in exactly two places — its Dockerfile and its driver — and it runs the very same greeting specification, with the gRPC server delegating to the one domain function that already served HTTP. No new tests, no new domain logic. Docker startup is shared and a single parameterised Dockerfile covers both.
The close: making the practice livable, and knowing when to stop. The final movement is about cost control and judgement. First, mechanics: the slow acceptance tests are wired to Go's -short flag, and a Makefile exposes a fast unit-test target alongside a separate full-build target — which only works because the slow tests genuinely check the flag and skip themselves, so a fresh checkout is testable with almost no setup. Then judgement: heuristics for when an acceptance test earns its keep. Genuinely new user-facing behaviour deserves one; variations of business rules and edge cases belong in unit tests, because piling another acceptance test onto an already-covered journey costs time and buys little, and acceptance tests give weak feedback on internal design. The author frames these as questions to ask yourself, not a rule. A new "Curse" feature then demonstrates the heuristic in action — it is genuinely new behaviour, so the specification comes first, the driver fails to compile, gets stubbed, the protobuf code is regenerated, and server and driver are implemented end-to-end. The chapter ends by recasting everything it has built: because the specifications were kept free of implementation detail, they read as documentation of what the system does.
One misreading worth naming. It would be easy to come away thinking the specification/driver split is needless indirection, and that a test written straight against the real UI or API is simpler. The chapter's whole argument is the opposite: the indirection is precisely what absorbs change. The driver is the single place that knows about tags, endpoints, and protocols — which is why swapping HTML tags or adding gRPC costs one file instead of a test suite.
Key claims, with the lines that support them
Acceptance tests become expensive mainly because they are tied to implementation details, not because they use too much abstraction.
The author's example is a Selenium test that breaks when markup is changed from one tag to another: nothing a user can see has changed, yet the test must be fixed, so the maintenance cost comes from the test knowing about internals.
“Too often, though, the latter is the reason acceptance tests have to change. To the point where engineers even become reluctant to change their system because of the perceived effort of updating tests!”
Learn Go with Tests - Scaling Acceptance Tests (and light intro to gRPC) › Tight-couplingRead in context →
A specification kept free of implementation detail can be run by different drivers against different environments, even production.
Building on the decoupling argument, the author points to a web page sitting on top of an API: one specification can be exercised through a headless browser and again through direct HTTP calls. The author adds the caveat that passing in a staging environment still does not prove production is correct.
“Decoupling how the specification is executed allows us to reuse it in different scenarios.”
Learn Go with Tests - Scaling Acceptance Tests (and light intro to gRPC) › Testing on steroidsRead in context →
Beginning with an acceptance test and working downward avoids wasted work.
From experience the author describes engineers building a chunk of code in isolation that turns out not to do what was wanted, carries work nobody needed, resists integration and gets rewritten; a failing acceptance test instead states the goal before any code exists. This is reported experience rather than measurement, and in the chapter it means paying for a Greeter specification, an HTTP driver, a Dockerfile and a container-based test before the first real feature.
“This is a crucial property of the acceptance-test-driven approach, using tests to get real validation of our code.”
Learn Go with Tests - Scaling Acceptance Tests (and light intro to gRPC) › The perils of bottom-upRead in context →
3 more claims
Adding a whole new transport should not require new tests or new domain logic.
The gRPC version of the greeter differs from the HTTP version only in its Dockerfile and its driver, and it calls the very same greeting specification; the gRPC server ends up delegating to the one domain function that already served HTTP.
“This new functionality can be accomplished by creating a new adapter to interact with our domain code.”
Learn Go with Tests - Scaling Acceptance Tests (and light intro to gRPC) › Write the test firstRead in context →
Slow acceptance tests should be skippable so that a fresh checkout can be tested with almost no setup.
The author wires the slow tests to Go's short-test flag and ships a Makefile where the unit-test target runs the fast tests only, with a separate target for the full build; this works only because the slow tests actually check the flag and skip themselves.
“It's preferable that running go test ./... should be runnable with no further set up from an engineer, beyond say a few key dependencies such as the Go compiler (obviously) and perhaps Docker.”
Learn Go with Tests - Scaling Acceptance Tests (and light intro to gRPC) › Separating different kinds of testsRead in context →
Brand-new user-facing behaviour deserves an acceptance test; variations of business rules and edge cases belong in unit tests.
The new Curse feature is genuinely new behaviour, so the author writes its specification first, lets the driver fail to compile, stubs it, regenerates the protobuf code and then implements server and driver. By contrast, piling another acceptance test onto a journey that is already covered costs time and buys little, so unit tests are used there. The author presents this as questions to ask yourself rather than a hard rule.
“Permutations of business rules and edge cases should be simple to drive via a unit test if you have separated concerns well.”
Learn Go with Tests - Scaling Acceptance Tests (and light intro to gRPC) › Enhance both systems by updating the domain logic with a unit testRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Three layers keep specs stable
This chapter splits an acceptance test into the specification (what behaviour is wanted), the driver (how to talk to the system), and the system itself. Because only the driver knows about markup, URLs or protocols, implementation churn lands there and the specification stays untouched unless the behaviour genuinely changes.
Mechanism 2
Changes start at the spec and push outward
When new behaviour is needed, the author edits the specification first, which immediately breaks compilation of the driver, and then the server. Each compiler error points at the next thing to change, so the work is driven by the desired behaviour rather than guessed from the bottom up.
Mechanism 3
New protocol is just a new adapter
The author treats transports like HTTP or gRPC as complexity that exists only because computers are involved, not as part of the domain. Keeping greeting logic in a core domain function and pushing the protocol into an adapter means a second transport needs only a new driver and Dockerfile, while the same specification and domain code are reused.
Common misreadings
Layers like a separate specification and driver are needless indirection; a test written straight against the real UI or API is simpler and easier to maintain.
The indirection is what absorbs change: the driver is the one place that knows about markup, endpoints and protocols, so refactoring the system touches only it, while a test written directly against the system breaks on cosmetic changes such as swapping one HTML tag for another.
“Too often, though, the latter is the reason acceptance tests have to change. To the point where engineers even become reluctant to change their system because of the perceived effort of updating tests!”
Learn Go with Tests - Scaling Acceptance Tests (and light intro to gRPC) › Tight-couplingRead in context →
Since acceptance tests exercise the real system, every new rule and edge case should get one.
Acceptance tests are slow and give weak feedback on internal design, so they are reserved for new user-facing behaviour; once a journey is already covered, another acceptance test adds cost with little value, and permutations of business rules are better driven through unit tests.
“Permutations of business rules and edge cases should be simple to drive via a unit test if you have separated concerns well.”
Learn Go with Tests - Scaling Acceptance Tests (and light intro to gRPC) › Enhance both systems by updating the domain logic with a unit testRead in context →
Key concepts in Testing fundamentals
- Acceptance test
- A black-box test checking the system's external behaviour from a business point of view. A black-box test that builds and runs the real program in a container and drives it through a specification. A test driving the whole system as a user would, through a driver against a specification.
- Where it applies: Not concerned with internal quality, so it cannot replace unit tests for design feedback. Slower than unit tests, so keep the boilerplate shared and the specification free of transport details. Slow and weak on internal design feedback; skip for edge cases.
- Specification
- A statement of wanted behaviour, free of transport or implementation detail, reusable across drivers.
- Where it applies: Only useful if the domain logic is actually modelled in code.
- Driver
- The adapter that turns specification calls into system-specific calls such as HTTP requests.
- Where it applies: Each system surface needs its own driver; it is the place implementation churn should land.
- Accidental complexity
- The complexity that comes only from using computers: networks, disks, APIs.
- Where it applies: Contrasted with essential complexity, the domain rules that predate computers.
- Adapter pattern
- A thin wrapper that reshapes something you already have so it satisfies an interface a caller demands.
- Where it applies: Only helps when the behaviour already exists but the shape is wrong; it adds no new logic.
- Ports and adapters
- A layout that keeps domain code at the core and pushes network, Docker and protocol concerns into adapters.
- Where it applies: This chapter warns against a dumping-ground domain folder; group related ideas such as interactions.
2 more concepts
- Test Pyramid
- A suite shape: many quick unit tests, only a few slow end-to-end acceptance tests.
- Where it applies: Guides proportions, not a rule for any single decision.
- Short test flag
- go test -short plus testing.Short() lets engineers run only the fast tests.
- Where it applies: Only works if slow tests actually check the flag and skip.
Reader’s notes on Testing fundamentalsAI reading companion
“When faced with a poor codebase without any tests, please resist the temptation to start refactoring.”
Read in context →The strongest practical advice in the opening. Refactoring without a net isn't refactoring, it's rewriting and hoping.
“engineers bike-shed over whether something should be an <article> or a <section> for the billionth time”
Read in context →The example is well-chosen because it's trivial — that's the point. Tests that break on semantically irrelevant markup are measuring the wrong thing.
“this statement was true before computers were even used in banks!”
Read in context →A clean test for whether something is essential complexity: would it survive the disappearance of all your machines?
17 more notes
“Dev/Prod parity is, at best, a white lie. I test in prod.”
Read in context →Deliberately provocative, and he earns it — but notice the sleight of hand: running read-only specs in prod is a very different thing from what most readers hear in "test in prod".
“Why not use the same specification to test both?”
Read in context →This is the real payoff of the whole architecture — the specification becomes an asset you amortize across layers, not a cost per layer.
“most of the complexity that lives internally within software doesn't usually reside in implementation detail but "between" the units and how they interact”
Read in context →Quietly the deepest line here. It also explains why unit tests alone feel reassuring yet miss the bugs that actually ship.
“they think is going to solve a job, but it:”
Read in context →The bottom-up critique lands, but it's argued from anecdote. The honest version is: bottom-up fails when you don't yet understand the problem — which is most of the time, not all of it.
“this level of ceremony to decouple our specification from implementation might make some people accuse us of "overly abstracting”
Read in context →He raises the objection himself because he knows the code so far — one hardcoded string across five files — makes it look damning. Pre-empting is smart; it isn't yet proof.
“so long as you're not doing anything with the err, tests that say "you return the error you get" are relatively low value”
Read in context →A refreshingly unorthodox concession from a TDD book. The criterion is behaviour, not coverage.
“don't write the production code that would make the test pass until we've verified the test fails as we'd expect”
Read in context →The red step is really a test of the test. Skipping it is how people end up with suites that pass unconditionally.
“This "iteration 0" ceremony and setup will serve us well for further iterations.”
Read in context →And the next section immediately demonstrates it — one spec edit, the compiler names every place to change. That's the argument the earlier promise was writing a cheque for.
“This would be nice, but it doesn't work”
Read in context →Ending the segment on a failure is a good teaching instinct — the specification wants an interface, the domain function is just a function. The seam is about to show.
“The value of design patterns is not specific implementations but a language to describe specific solutions to common problems engineers face.”
Read in context →The best one-line defense of patterns I know: they're vocabulary, not code. Half the eye-rolling comes from people shipping the diagram instead of speaking the word.
“type GreetAdapter func(name string) string”
Read in context →Go's method-on-a-func-type trick does in three lines what other languages need a class for. The adapter here is almost free — worth noticing that the pattern's weight is language-dependent.
“Now, imagine your CTO has now decided that gRPC is the future.”
Read in context →A nicely honest framing: the requirement is arbitrary, top-down, and not about the user at all. That's exactly the kind of change your architecture is supposed to absorb without flinching.
“Don't be lazy and have a domain folder in your projects with hundreds of unrelated types and functions.”
Read in context →The jab lands because domain/ is the junk drawer of every DDD-flavored repo. Though interactions is itself a pretty thin name — the advice is sounder than the example.
“not by coincidence but by design”
Read in context →The payoff sentence of the whole chapter. Duplicated-looking test shapes are the visible proof that the specification actually factored out.
“todo: we shouldn't redial every time we call greet, refactor out when we're green”
Read in context →Leaving the TODO in the printed code is pedagogically brave — it shows the author knowing better and deliberately deferring. That discipline is the real lesson, not sync.Once.
“I'd like to run our acceptance test first to see if everything is working on a transport level and verify the failing test output”
Read in context →"fixme" as a tracer bullet: prove the pipes before the logic. Separating transport failure from domain failure is what keeps red tests informative.
“Is this something that the non-computer people talk about a lot?”
Read in context →The sharpest heuristic on the page — test boundaries drawn by who talks about the behaviour, not by code structure.
A reader’s takeAI reading companion
This chapter is really one argument made three times at increasing stakes, and reading it as a single piece makes the shape clear: acceptance tests rot when they are coupled to how the system happens to be built today, so you split them into specification (what the business means), DSL/driver (how you reach the system), and implementation (whatever it currently is). The opening earns that claim with well-chosen trivia — the <article>-vs-<section> bike-shed, the banking rule that was true before computers existed in banks — because the test of essential complexity is whether a statement survives the disappearance of all your machines. The warning not to refactor an untested codebase is the most practically useful line in the chapter and deserves to be read literally: without a net it isn't refactoring, it's rewriting and hoping. The 'test in prod' provocation is the one place I'd ask the reader to slow down — read-only specs run against production is a far narrower and more defensible practice than the phrase lets most people hear.
The middle is where the chapter stops asserting and starts paying. Iteration 0 looks absurd: one hardcoded greeting spread across five files, enough ceremony that the author pre-empts the 'over-abstraction' charge himself — correctly, but pre-emption is not proof. The proof arrives in the next beat, when adding a name parameter means editing the specification once and letting the compiler name every site that must change. Then the domain function won't satisfy the specification's interface, the seam shows, and the adapter pattern slides in — notably almost free in Go, since a method on a func type does in three lines what other languages stage as a class, which is a real reminder that a pattern's weight is language-dependent. The gRPC turn is the honest climax: an arbitrary CTO-driven transport change, absorbed by writing a second driver while specification and domain code stay untouched. 'Not by coincidence but by design' is the chapter's true payoff sentence, and the amortization insight — one specification reused across layers, an asset rather than a per-layer cost — is the architectural reason the whole exercise is worth its ceremony. The deepest line, almost buried, is that software's complexity lives between units rather than inside them; that single observation explains why a green unit-test suite can feel reassuring and still miss everything that actually breaks in production.
Continue reading
The close is more grounded than most books manage. Separating slow acceptance tests from unit tests via -short is mundane plumbing that makes the whole scheme survivable in CI. The heuristics are better than the usual testing-pyramid liturgy: 'is this something non-computer people talk about a lot?' draws test boundaries by who discusses the behaviour rather than by code structure, and the admission that 'you return the error you get' tests are low-value is a genuinely unorthodox concession from a TDD author — behaviour, not coverage, is the criterion. The Curse feature then replays the full loop end-to-end as practice, and the chapter lands on specifications-as-documentation, which is the one claim it mostly asserts rather than demonstrates.
What I'd flag for the reader: the bottom-up critique is argued from anecdote, and the fair version is that bottom-up fails when you don't yet understand the problem — which is most of the time, not all of it. The scolding of junk-drawer domain/ folders lands, though 'interactions' is itself a thin name; take the advice, not the example. And notice the pedagogical choices rather than just the code — the red step as a test of the test, the 'fixme' tracer bullet that proves the transport before the logic so a red test tells you which layer failed, the TODO about redialing left visibly in print. That last one is the quiet lesson: the discipline of knowing better and deliberately deferring, in writing, beats the sync.Once it eventually becomes. Read this chapter for the separation of specification from driver and for its vocabulary — patterns as language, not as diagrams to ship — and accept that on a one-greeting program the structure will look like overkill. It is a cheque the gRPC section cashes.
Read Testing fundamentals with these cards beside each passage.
Open in FeedLitraTesting fundamentals: Working without mocks, stubs and spies
How much behaviour must a fake replicate before it stops being cheaper than the real dependency?
How Testing fundamentals unfoldsAI-assisted
This chapter opens with a grievance dressed as a war story. The author recalls a team wiring their system into six external APIs — badly documented, constantly shifting — and the test doubles they built to stand in for them. The stubs and mocks drifted away from the real APIs; tests became flaky; the team's days drained into rediscovering how the dependencies actually behaved and then re-editing the same duplicated stub code in a dozen places. The author flags the anecdote as simplified, but it carries the chapter's framing claim: mocks, stubs and spies scatter untested guesses about your dependencies across the whole suite, so the suite can stay green while the software is broken. A second, sharper limitation follows. A stub hands back a canned value and remembers nothing, so a flow that creates a resource, retains its ID and updates it later has nowhere to keep that state. The moment a test spans several units and several stateful calls, the stub cannot follow the story — and that, the chapter notes, is exactly where integration tests are wanted.
From that diagnosis the chapter develops its alternative: the stateful in-memory fake. Because a fake holds state, a test can assemble real collaborators and then assert on the end state rather than spying on traffic. The worked example puts two fakes into a customer service and simply asks each fake what it now holds to confirm the social security number was recorded; the author argues this reads more easily than asserting which functions were called with which arguments, and it leaves the service's internals private instead of freezing them into the test. The result is broad integration coverage at something close to unit-test speed. But a fake is only a second set of assumptions unless something checks it, which is where contracts enter: a test written against the interface, so the same suite can run against both the fake and the real dependency. The sample contract for the first API captures ordinary create/get/update behaviour alongside a surprising real rule — the name "Dave" is rejected — and the in-memory fake is forced to obey it. New knowledge about a dependency becomes one disciplined edit that every test inherits, rather than a manual hunt through the suite. Two practical notes close out the mechanics. Because a fake is a genuine working implementation, its error branches are hard to reach, so the chapter wraps it in a decorator holding the fake plus optional per-method function fields: normal calls pass straight through, and a single override makes the dependency fail on cue. And database code is treated the same way — one contract, run against both the in-memory implementation and a real SQLite-backed one, with the slower real runs reserved for validating the contract rather than used everywhere. Proving that a particular SQL string was assembled, the author insists, proves nothing about whether the code behaves correctly.
The turn comes when the author takes up the obvious objection: all this is extra code. The answer reframes the ledger. Automated tests never reach the customer, so their entire value lies in how efficiently engineers can work — the extra code is an investment, not waste. In-memory fakes plus clearly separated modules let you isolate the actors in a scenario and replay it repeatedly and cheaply; the alternative is slow black-box or manual testing on shared environments. The author is candid that this payoff is conditional: the code must already be modular enough to isolate, and the contract must actually be runnable against the real system. A related warning lands here too — once you have good fakes, it is tempting to stop exercising the real dependency, but a fake is trustworthy only while the contract is still being executed against the live thing. Skip that and you are back in the original trap of assumptions nobody validated, which is also why the contract must target an interface rather than an implementation.
The chapter closes by placing the practice in a workflow and then in an organisation. Contracts should fall out of the work top-down, not be designed first: write a failing acceptance test, put the collaborator behind an interface, satisfy it with an in-memory fake, write down the assumptions you made as a contract, then make the real dependency pass that contract and ship. Starting from the contract is bottom-up design and invites over-engineering for requirements that may never arrive. Scaled up to teams, contracts checked into build pipelines do what wiki pages and meetings cannot. Agreeing an API up front rarely holds, because the act of writing code deepens your understanding of the domain and invalidates your earlier assumptions — which then forces change on the other team. A contract running in both pipelines surfaces that change as a failing build rather than a Slack thread or a weekly meeting, and so lets modular teams absorb cross-team change they could not have predicted.
Key claims, with the lines that support them
Mocks, stubs and spies scatter untested guesses about your dependencies all over the test suite, so the suite can stay green while the software is actually broken.
The author recounts a team wiring up six badly documented, constantly changing external APIs: the doubles drifted from reality, tests turned flaky, and much of the team's time went into rediscovering how the APIs behaved and re-editing the same duplicated stubs in many places. It is an anecdote, simplified for the telling, but it shows how the cost lands.
“If there is a discrepancy between the behaviour of the double and the real dependency, or if one happens over time (e.g. the real dependency changes, which has to be expected), you may have passing tests but failing software.”
Working without mocks, stubs and spies › The problem with stubs and mocksRead in context →
Building on that problem, a stateful fake gives you broad integration coverage at close to unit-test speed, because you check the end state instead of spying on calls.
In the worked example the test assembles two fakes into the customer service and then simply asks each fake what it now holds to confirm the social security number was recorded. The author argues this reads more easily than asserting which functions were called with which arguments, and it leaves the service's internals private rather than freezing them into the test.
“Using fakes, we can make assertions based on the final states of the respective systems rather than relying on complicated spying.”
Working without mocks, stubs and spies › Enter: FakesRead in context →
Assumptions about a dependency should be written as a contract — a reusable test run against both the fake and the real thing — so new knowledge means one disciplined edit.
The sample contract for the first API captures ordinary create/get/update behaviour alongside an unexpected real rule (the name 'Dave' is rejected), and the in-memory fake is forced to obey it. Without this, the author notes, you would have to remember to hunt down every affected test by hand. The price is setup effort plus the ability to actually run the contract against the real system.
“Without this approach, you'd have to remember to find all the relevant tests and update the stubs. Error-prone, labourious and boring.”
Working without mocks, stubs and spies › Evolving softwareRead in context →
3 more claims
Turning the objection around: the extra code for fakes and contracts is an investment rather than waste, because tests exist to make engineers productive, not to ship features.
Automated tests never reach the customer directly, so their value is purely in how efficiently the team can work. The author reasons that in-memory fakes plus clearly separated modules let you isolate the actors involved and replay a scenario repeatedly and cheaply, whereas the alternative is slow black-box or manual testing on shared environments. The benefit depends on the code already being modular enough to isolate.
“Automated tests do not directly benefit customers, but we write them to make ourselves more efficient with our work (you don't write tests to chase coverage scores, right?).”
Working without mocks, stubs and spies › Isn't this extra code waste?Read in context →
Contracts should fall out of the work top-down, starting from an acceptance test and a fake, instead of being designed first.
The author lays out the order: write a failing acceptance test, put the collaborator behind an interface, satisfy it with an in-memory fake, then write down the assumptions you made as a contract, then make the real dependency pass that contract and ship. Beginning from the contract instead is bottom-up design and invites over-engineering for requirements that may never arrive.
“To push to production, you can't use in-memory! Encode the assumptions you made against the fake into a contract.”
Working without mocks, stubs and spies › How does this fit into TDD?Read in context →
Between teams, contracts checked into build pipelines do the job that wiki pages and meetings cannot: they let each team keep moving while the shared boundary shifts.
The author's reasoning is that agreeing an API up front rarely holds, since the act of writing code deepens your understanding of the domain and invalidates earlier assumptions, which then forces changes on the other team. A contract that runs in both teams' pipelines surfaces such a change as a failing build rather than a Slack thread or a weekly meeting.
“Don't rely on weekly meetings or Slack threads to flesh out changes. Codify your assumptions in contracts.”
Working without mocks, stubs and spies › Wrapping upRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Why stubs break across units
A stub only hands back a canned value and remembers nothing, so a flow that creates a resource, keeps its ID and updates it later has nowhere to store that state. Once a test spans several units and several stateful calls, the stub cannot follow the story — and that is precisely where integration tests are needed.
Mechanism 2
How contracts keep fakes honest
A contract is a test written against the interface, so the same suite can be run against both the in-memory fake and the real dependency. When you discover a surprising real behaviour, you encode it once in the contract; the fake must then change to pass, and every test using that fake inherits the correction instead of quietly drifting.
Mechanism 3
Decorators reach failure paths
A fake is a genuine working implementation, so its error branches are hard to reach from a test. Wrapping it in a decorator that holds the fake plus optional per-method function fields keeps normal calls passing straight through, while a single override makes the dependency fail on cue without editing the fake itself.
Common misreadings
Testing database code means mocking the driver and checking that the right SQL statement came out.
What matters is whether the code behaves correctly, and proving a particular SQL string was assembled proves nothing about that. The author instead writes one contract and runs it against both the in-memory implementation and the real SQLite-backed one; because the real runs are slower, they are kept to validating the contract rather than used everywhere.
“Proving a specific SQL statement was compiled does not prove your code behaves how you need it to.”
Working without mocks, stubs and spies › Where's the chapter on testing databases?Read in context →
Once you have good fakes, the fakes are the test suite — you can stop exercising the real dependency.
A fake is only trustworthy while something checks it against reality, and that something is the contract being executed against the live dependency as well. Skip that step and you are back to the original trap of assumptions nobody ever validated; this is also why the contract has to target an interface, so both implementations can be fed through it.
“Without this approach, you'd have to remember to find all the relevant tests and update the stubs. Error-prone, labourious and boring.”
Working without mocks, stubs and spies › Evolving softwareRead in context →
Key concepts in Testing fundamentals
- Test Double
- Any controllable stand-in for a real dependency used while testing a subject under test. Any stand-in for a real dependency in tests; this chapter prefers contract-backed fakes over ad-hoc stubs and spies.
- Where it applies: Useful when the real dependency is slow, remote, or hard to push off the happy path. Stubs and spies still have a place for simulating one-off behaviours, if costs stay controlled.
- Fake
- A genuine but lightweight, stateful implementation of a dependency, often in memory.
- Where it applies: Costs more code than stubs and must be hand-written because it encodes real behaviour.
- Contract
- A reusable test written against an interface, run against both the fake and the real dependency to validate assumptions. A reusable test suite that any implementation of an interface must pass, so fakes and real dependencies behave alike.
- Where it applies: Only works if the test targets an interface rather than a concrete type. This chapter advises not starting from the contract; derive it after a fake validated the design.
- Integration tests
- Tests proving two or more units work correctly once combined, sitting between unit and acceptance tests.
- Where it applies: Needed when unit tests are too narrow but full black-box acceptance tests are too costly.
- Decorator pattern
- Wrapping a unit of code to override or extend its behaviour without editing its source.
- Where it applies: Used here to force fakes into failure paths that are otherwise hidden.
- Modular system
- Code split into well-tested parts with trustworthy fakes, so you can recompose and experiment cheaply.
- Where it applies: The chapter's benefits depend on this modularity; tangled code cannot be isolated this way.
0 more concepts
Reader’s notes on Testing fundamentalsAI reading companion
“These assumptions are usually not validated beyond manual checking, so they threaten your test suite's usefulness”
Read in context →The whole chapter's thesis in one line: the problem isn't mocks per se, it's unverified belief hardcoded in dozens of places.
“Decoupling your build from another team's”
Read in context →Note this is an organizational reason wearing technical clothes. Test doubles as political insulation.
“Any resemblance to actual persons, living or dead, is purely coincidental.”
Read in context →A joke that leaks real frustration. The disclaimer protects colleagues he clearly spent months chasing on Slack.
15 more notes
“there's a real risk of neglecting to update it in other stubs representing the same dependency”
Read in context →This is the actual pain, and it's duplication, not mocking. Same knowledge copied N times with no single owner.
“if we manually checked our system, we would query those APIs to check their state, not inspect our request logs”
Read in context →Strong argument: assert what a human would check. Spies make you assert the mechanism, not the outcome.
“the system will not allow you to add 'Dave' as a customer”
Read in context →The best thing in the chapter. The contract becomes a museum of real-world absurdity — knowledge that would otherwise die in a Slack thread.
“Running the full test suite before checking in may result in other tests failing due to the fake having a different behaviour. This is a good thing!”
Read in context →Here's the real payoff: one fake becomes a blast radius detector. Centralized assumptions mean a change propagates automatically instead of being hunted.
“We often identified issues before the other systems' developers did.”
Read in context →Slipped in casually but it's the strongest empirical claim here — contracts turned into a monitoring tool for other people's APIs.
“you are losing the guarantees from your contract as you are introducing ad-hoc behaviour”
Read in context →Credit for admitting it: the moment you need error paths, the stub sneaks back in through the decorator. The approach doesn't fully escape what it criticizes.
“Proving a specific SQL statement was compiled does not prove your code behaves how you need it to.”
Read in context →And this quietly answers why he refused the database chapter for five years — the question itself was framed wrong.
“Fakes with contracts allow you to use restrict the need to use the "heavier" dependency to only when you're validating the contract”
Read in context →The typo survived publication, but the economics is the real point: the expensive dependency is paid once, at the contract boundary, not on every test run.
“define some API contracts (often on a wiki page!) and then work independently for six months and stick it all together”
Read in context →The parenthetical does all the work. A wiki page is a contract that cannot fail a build — which means it isn't a contract, it's a hope.
“their ability to make progress and to change their minds, without asking for permission from any person or group outside of their small team”
Read in context →Clever placement: the quote makes a technical testing trick into an organizational argument. Fakes are a form of autonomy.
“Don't rely on weekly meetings or Slack threads to flesh out changes. Codify your assumptions in contracts.”
Read in context →The sharpest line in the chapter — communication doesn't scale, but a failing test does. Though it quietly assumes the other team agrees to run your contract.
“weird, hypothetical (but possible) scenarios into self-contained tests to help us understand the problem”
Read in context →Here tests stop being verification and become a thinking instrument. That's the real payoff of cheap doubles, and it's buried mid-paragraph.
“Of course, stubs and spies have their place.”
Read in context →The concession arrives only after the case is won — rhetorically shrewd, but a chapter titled "working without" owes it more than one sentence.
“it's hard to reproduce the exact behaviours of a system written by other engineers, who also change it simultaneously”
Read in context →This is the honest admission that undercuts the whole scheme: a fake is only as current as the contract someone remembered to update.
“The problem is this doesn't give you isolated feedback, and the feedback is slow.”
Read in context →Shared staging environments as the villain — every engineer who has waited their turn to deploy will nod. The diagnosis is social, not technical.
A reader’s takeAI reading companion
This chapter opens not with a technique but with a grievance, and that's the key to reading it well. This chapter begins by locating the real defect in conventional test doubles: each stub or spy hardcodes a belief about a dependency, those beliefs are never validated beyond someone's manual poke at a staging box, and they are duplicated across dozens of test files with no single owner. The thesis line — assumptions 'not validated beyond manual checking' threaten the suite's usefulness — is the whole argument compressed. Notice what follows from it: the villain is not mocking, it's unverified duplicated knowledge.
That reframing matters, because it means the chapter's remedy would still be needed even if mocks had better ergonomics. From there the piece unfolds in a clean arc — diagnose the scattered-assumption problem, propose the stateful in-memory fake as the single place that knowledge lives, bolt a contract onto it so the fake is verified against the real thing rather than merely believed, then show what that buys: broad fast integration tests, assertions on state instead of on call logs, a suite that runs on a laptop, and decorators to reach the error paths a fake can't naturally produce. The closing stretch widens the lens from testing to organization, defends the upfront cost, and plants the approach inside an acceptance-test-first TDD loop.
Continue reading
The strongest moves are the ones about epistemics, not tooling. Assert what a human would check — you'd query the API's state, not inspect your own request logs — is a genuinely clarifying rule, and it explains in one stroke why spy-heavy tests feel busy yet hollow: they prove the mechanism fired, not that the world changed. The contract-as-museum idea is the chapter's best gift: the rule that 'the system will not allow you to add Dave as a customer' is exactly the kind of absurd, hard-won, real-world knowledge that normally dies in a Slack thread, and a contract gives it somewhere to live where it can fail a build.
Likewise the apparently throwaway admission that other tests breaking after a fake changes is 'a good thing' — that's the actual payoff, a blast-radius detector, assumptions centralized so a change propagates instead of being hunted. And the casual 'we often identified issues before the other systems' developers did' is the chapter's strongest empirical claim, almost hidden: contracts quietly became monitoring for someone else's API. The sharpest line of all is the imperative to stop relying on weekly meetings and Slack threads and codify assumptions in contracts — communication doesn't scale, a failing test does.
What I saw through: a good deal of this chapter is an organizational argument wearing technical clothes. 'Decoupling your build from another team's' is politics; the quoted praise for teams that can change their minds without permission is an autonomy manifesto; shared staging environments are cast as the villain and the diagnosis there is social, not technical. The wiki-page jab is the tell — a contract that cannot fail a build is not a contract, it's a hope — and the parenthetical does more work than the paragraph around it. That's not a flaw; it's the chapter's real insight, and the author would be stronger for saying it outright: fakes-plus-contracts is a coordination technology that happens to be made of test code.
But the scheme has seams, and to his credit the author names most of them rather than hiding them. The moment you need error paths, the stub walks back in through the decorator — he admits you're 'losing the guarantees from your contract' there, which concedes that the approach doesn't fully escape what it criticizes. More damaging, he admits it's hard to reproduce the behaviour of a system other engineers are changing simultaneously. That's the load-bearing weakness: a fake is only as current as the contract someone remembered to update, and the sharpest line in the chapter quietly assumes the other team agrees to run your contract against their service.
If they don't, you have merely rebuilt the wiki page in Go — prettier, executable against your own fake, and just as capable of being stale. The economics also deserve more credit than the typo-ridden sentence gives them: the expensive dependency is paid once at the contract boundary, not on every test run. That, not purity, is why this scales. Two criticisms of the writing. The concession that 'stubs and spies have their place' arrives in a single sentence after the case is already won — rhetorically shrewd, but a chapter titled 'working without' owes its opposition a real hearing, including when a fake is simply too costly to build.
And the most interesting idea is buried mid-paragraph: that turning weird hypothetical scenarios into self-contained tests makes tests a thinking instrument rather than a verification chore. That is the deepest consequence of making doubles cheap and it deserved a heading. The five-year refusal to write a database chapter also gets its answer here, almost in passing: proving a specific SQL statement was compiled proves nothing about behaviour, so the question was framed wrong all along. What I'd tell a reader: don't take this as a ban on mocks. Take the diagnostic. Ask of every test double in your codebase — who owns this assumption, how many copies exist, and what would fail if reality drifted?
If the answer is 'nobody, many, nothing,' you have the problem this chapter describes, whatever your tooling. Then ask the harder question the chapter doesn't press hard enough: do you have the organizational agreement to keep a contract honest? Build the fake if you do. If you don't, build the contract anyway and let its failures be the lever that gets you the agreement — but go in knowing that a fake nobody verifies is just a stub with better manners.
Read Testing fundamentals with these cards beside each passage.
Open in FeedLitraTesting fundamentals: Refactoring step, starting checklist
Which checklist item would most improve the code you touched today?
How Testing fundamentals unfoldsAI-assisted
The chapter opens by framing refactoring as a skill — something that becomes near-second-nature with practice — and immediately draws the boundary that organizes everything after it: refactoring improves existing code without changing behaviour, so your tests shouldn't need to change. That definition is why it sits as the third step of the TDD cycle, and it doubles as a diagnostic: if you're changing tests while "refactoring," you're doing something else. The author then works that distinction through a concrete case, a birthday-greeter method with an overlong argument list that one might want to collapse into a single Person parameter. Because tests are coupled to behaviour and method signatures, that's design, not refactoring — and the recommended response is not to abandon TDD but to apply it, driving the change test-first in small steps (commenting out all but one of several affected tests, then working through the rest). From there the chapter widens briefly to "big design," which is longer, more conversational, and more subjective, but still worth breaking into risk-reducing steps.
The middle section supplies the argument for why all this small-scale discipline matters, using the "can't see the wood for the trees" idiom: you can only have good conversations about overall design if the underlying code is well-factored, because otherwise everyone burns their attention mentally parsing a mess, and badly-factored code compounds as complexity piles onto shaky foundations. That motivation leads into the chapter's practical core — a mental checklist to run every TDD cycle, annotated with IntelliJ/GoLand shortcuts the author teaches new engineers for muscle memory. The items are: inline variables that merely pass a value along (with the caveat not to chase clever one-liners when a name carries meaning); extract variables to DRY up repeated values; and then a notably balanced digression on DRY itself — misapplied, it yields tangled abstractions chasing fewer lines, but refusing to DRY anything is equally harmful, so the real goal is capturing an idea in one place, judged case by case, with a messy parameter list as the usual tell that you've gone wrong.
The checklist continues with extracting "magic" values into named variables or constants, illustrated by three HTTP clients sharing a one-second timeout — a case the author uses to flag that extraction also introduces coupling, since the three clients now necessarily share a value, and inlining can undo it. Then comes making public methods easy to scan: a widget-creation example is tightened by inlining a payload variable and extracting the JSON-building noise into a helper, so the public method says what it does rather than how. Fowler is quoted in support, and the author answers the standard objection ("now I can't see how it works") bluntly — learn to navigate with your tooling; jump to the symbol and jump back. A related move follows: values that don't depend on a method's arguments, like a constructed URL, can become fields computed at construction time. A before/after comparison shows the cumulative payoff — a modest but real improvement achievable in about a minute when tests provide the safety net — and a final checklist item suggests writing a method instead of a comment.
The chapter then turns to its own exceptions and tooling. Some changes do touch tests — renaming a public symbol being the obvious one — and the author is content to call these refactoring anyway, because automated IDE operations don't drag you into the spiral of simultaneously broken tests and production code. That leads to the practice advice: run unit tests after every small change (the millisecond feedback loop is what all that testability investment bought), and lean on source control so experimenting feels cheap rather than momentous. The author's pointed claim is that developers who say they lack time to refactor lack practice and discipline, and that a tool which makes extraction laborious will quietly make you refactor less.
It closes on permission and ethos. Refactoring shouldn't be requested or deferred; done little and often it isn't a time sink, whereas skipping it erodes internal quality, team capacity, and schedule slack. A closing Fowler quote inverts the usual excuse — a bout of refactoring tends to raise productivity, and feeling short of time is itself a sign you need to refactor. The wrap-up admits the list is only a start (pointing to Fowler's Refactoring, 2nd ed.), restates that too many treat refactoring as someone else's decision rather than a skill to learn, and ends with the loop that holds the chapter together: good refactoring makes code understandable, understandable code makes better designs visible, and so frequent small refactoring is a precondition for good design.
Key claims, with the lines that support them
If improving some code forces you to edit its tests at the same moment, you are not refactoring but doing something else.
The author derives this from coupling: tests are written against behaviour and signatures, so a true refactor can only touch internals — private methods, fields, new types — and therefore leaves tests alone by construction. He allows one narrow exception: an IDE-automated rename of a public symbol does change both production and test code, yet counts as refactoring because the tool performs it safely and it cannot spiral into cascading breakage.
“You're doing something else if you are "refactoring" some code and having to change tests at the same time.”
Refactoring step, starting checklist › Refactoring vs other activitiesRead in context →
Changing a method's signature is a design change and deserves the same test-first discipline as any new behaviour.
The author uses a birthday-greeter method with a long argument list that he wants to collapse into a single Person parameter. Because real callers, not just tests, depend on that signature, he drives the change test-first in small steps, and suggests commenting out all but one of the calling tests, making that one pass, then working through the rest — TDD's usual risk reduction applied to a change that merely feels different.
“You're doing something else if you are "refactoring" some code and having to change tests at the same time.”
Refactoring step, starting checklist › Refactoring vs other activitiesRead in context →
Public methods should announce what they do, leaving the how to private helpers.
He walks through a widget-creation method cluttered with JSON-string and byte-buffer ceremony: inlining the payload variable, then extracting the payload construction into its own function, leaves a method body that reads as three named intentions. Moving the URL, which depends on no argument, into a constructor-assigned field removes another line. The author's point, echoing Fowler, is that having to think in order to understand code is itself the signal to refactor.
“If we fail to address the minor design issues, we'll find it hard to engineer the overall design of our more extensive system.”
Refactoring step, starting checklist › Seeing the wood for the treesRead in context →
2 more claims
Well-factored code is the precondition for having useful conversations about larger design.
The argument runs through readability: colleagues who must decode a mess every time they open a file have no capacity left to reason about structure, and badly-factored code degrades exponentially as complexity stacks on weak foundations. Small cleanups also surface design questions directly — once steps carry names, you can ask why this type does X at all, or whether the work belongs elsewhere.
“If we fail to address the minor design issues, we'll find it hard to engineer the overall design of our more extensive system.”
Refactoring step, starting checklist › Seeing the wood for the treesRead in context →
Having no time is not grounds for skipping refactoring; it is usually the symptom that refactoring is overdue.
The widget example shows the economics: the whole improvement is a handful of tool-driven moves that take barely a minute when practised, with unit tests running in milliseconds as the safety net. The author adds that developers who complain refactoring is slow are slow at it precisely because they do it rarely and without discipline, and quotes Fowler that a bout of refactoring has repeatedly raised productivity — with the sole concession of being very close to a deadline.
“Not having enough time usually is a sign that you need to do some refactoring.”
Refactoring step, starting checklist › Don't ask permission to refactorRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Why tests stay untouched
Tests bind to two things only: observable behaviour and method signatures. So long as a change leaves both alone, you can add private helpers, fields or types and rewrite the insides of public methods freely, and the existing tests keep passing — which is exactly why this activity sits as the third step of the TDD cycle.
Mechanism 2
Naming a value also ties its users together
Pulling a repeated literal into one named variable removes the unexplained magic, but every place that now reads that name shares a single value. The author's three HTTP clients, after a shared timeout variable is extracted, can no longer be tuned independently unless you inline it again — so the gain in meaning comes with new coupling you should choose deliberately.
Mechanism 3
Small messes crowd out big design
If every file costs effort just to parse mentally, there is no attention left for questions about overall structure. Worse, new complexity gets piled onto those shaky foundations, so the decay compounds rather than accumulating evenly — hence continuous small cleanup is what keeps larger design work possible at all.
Common misreadings
DRY has been discredited, so the safe move is to leave duplicated code and values alone.
The real idea of DRY is capturing one idea in one place; shorter code is a by-product, not the target. Tangled abstractions built to save lines are a misapplication, but blanket refusal to consolidate leaves noise and higher maintenance cost, which the author calls a problem of its own. He suggests judging each case — ask whether the extracted function's parameter list reads sensibly and whether it names a real idea — and simply trying it, since version control and fast tests let you revert cheaply. If the consolidation feels hard, that is the cue to stop.
“Reducing the number of lines of code is often a side-effect of DRY, but it is not the actual goal.”
Refactoring step, starting checklist › DRY up stuff in generalRead in context →
Splitting a method into small private functions hides how the code actually works and makes it harder to follow.
The detail is deliberately kept out of the narration because it is irrelevant noise most of the time; when a reader does care, navigate-to-symbol jumps straight into the helper and one more keystroke returns. The author treats the objection largely as a tooling-fluency gap, and points out the same extract-method move is the better answer whenever you feel tempted to write an explanatory comment.
“If you are well-practised, this kind of improvement will barely take you a minute, and so long as you have applied TDD well, you'll have the safety net of tests to ensure you're not breaking anything.”
Refactoring step, starting checklist › Comparing and contrasting CreateWidgetRead in context →
Key concepts in Testing fundamentals
- Refactoring
- Improving existing code without changing behaviour, so tests stay untouched.
- Where it applies: Changing behaviour or method signatures is design work, not refactoring; safe automated renames are an accepted exception.
- Big design
- Larger, partly subjective structural change that does require test changes and should be driven by TDD in small steps.
- Where it applies: Applies when signatures or behaviour must change; still needs small risk-reducing steps.
- Magic numbers as named constants
- Replacing unexplained repeated values with named variables or constants to give them meaning.
- Where it applies: Extraction also couples every user of that value, which may not be wanted.
- DRY
- Capturing an idea in one place; fewer lines is a side-effect, not the goal.
- Where it applies: Over-applied DRY creates tangled abstractions; refusing to DRY at all also harms maintenance.
0 more concepts
Reader’s notes on Testing fundamentalsAI reading companion
“You're doing something else if you are "refactoring" some code and having to change tests at the same time.”
Read in context →A crisp, falsifiable definition — the test suite becomes the arbiter of what word you're allowed to use. Most refactoring arguments dissolve once you accept this.
“You can split hairs over whether this is a "behaviour" change, but you want your method to behave differently”
Read in context →Honest hedge. He knows "behaviour" is doing a lot of unpaid labor in his taxonomy, and chooses pragmatism over precision.
“I'd suggest commenting out all but one of the tests, driving out the change”
Read in context →The most practical sentence in the chapter, and the only one that admits mid-refactor your suite is temporarily lying to you. Risky advice if you forget to uncomment.
6 more notes
“badly-factored code gets exponentially worse as engineers pile on complexity on top of shaky foundations”
Read in context →"Exponentially" is rhetoric, not measurement. But the mechanism he names — no mental budget left for design because parsing eats it all — is the real argument and it doesn't need the math.
“we have also made it so all three clients share the same timeout”
Read in context →The best move in the chapter: he shows DRY creating coupling in the same breath as recommending it. Extraction is never free — it asserts that two values are the same idea, not just the same number.
“Have you learned how to navigate codebases using your tooling effectively?”
Read in context →He calls it blunt; it's also a dodge. Tooling answers "where is it" but not "how many layers deep do I have to chase this." The objection to small functions is about working memory, not keybindings.
“any changes you can safely perform with your IDE/editor, I would still happily call refactoring”
Read in context →So the real criterion was never "tests don't change" — it was "the risk is mechanically bounded." Automation, not test stability, is the load-bearing idea. The earlier rule was a proxy.
“it takes so much time for them because they don't do it with discipline - and they have not practised it enough”
Read in context →Circular in a useful way: unpractised refactoring is slow, slowness discourages practice. He's describing a trap, though he frames it as a character flaw.
“Other than when you are very close to a deadline, however, you should not put off refactoring”
Read in context →Note Fowler's own escape clause, quoted intact. Even the patron saint grants that deadlines are real — which is more concession than "don't ask permission" allows.
A reader’s takeAI reading companion
The chapter's strength is narrowness: it refuses to let 'refactoring' mean 'any code change I feel like making,' and gives a mechanical test — did your tests have to change? — to enforce the line. That discipline is what makes the rest teachable. But the chapter quietly revises its own criterion by the end: renaming a public symbol does change tests, and is still called refactoring because the IDE guarantees it. So the true principle is bounded mechanical risk, with test-stability as a convenient proxy; stating that up front would have been stronger than arriving at it as an 'exception.' The finest passage is the HTTP timeout example, where extraction is recommended and its hidden cost — three clients now coupled into one decision — is shown in the same motion; that's a rare piece of honest teaching about abstraction.
Weakest is the reply to 'now I don't know how it works,' where a legitimate complaint about working memory and call-depth is answered with a jab about keybindings. And the closing moralism about developers who 'haven't got time' describes a self-reinforcing trap but attributes it to indiscipline, which is less useful than the loop itself. Overall: a good checklist, a better argument about why minor design debt forecloses major design thinking, and a definition that doesn't quite survive its own chapter.
Read Testing fundamentals with these cards beside each passage.
Open in FeedLitraBuild an application: Build an application
How does a product owner's changing demands shape test design?
How Build an application unfoldsAI-assisted
How it opens. The chapter begins not with code but with a checkpoint on the reader's progress. It assumes you have "digested" the preceding Go Fundamentals section, and credits you with two things as a result: a working command of most of the language's features, and a grasp of how to practise test-driven development. That assumption is the hinge on which the whole section turns — the opening exists to mark a boundary between learning Go in isolated exercises and putting it to use. The announcement that follows is deliberately plain: this next section will involve building an application.
How it develops. The middle of the chapter explains the method rather than the subject matter. Each chapter is to iterate on the one before it, widening the application's functionality "as our product owner dictates" — a small piece of framing that casts the reader as a developer receiving changing requirements rather than a student working through a syllabus. The text then sets expectations about content: new concepts will appear where they help you write good code, but the bulk of the new material will be about what Go's standard library already makes possible. This is the chapter's quiet argument — that building something real in Go is less about accumulating language tricks than about learning to reach for what ships with the language. The stated payoff follows directly: by the end you should understand how to grow an application in Go iteratively, with tests underpinning it.
The turn and the close. The chapter then shifts register entirely, from prose to a map. The remainder is a list of the six chapters ahead, each with a one-line description of what it builds and what it teaches: an HTTP server that listens for requests and answers them; JSON responses together with routing and embedding; reading and writing data on disk, plus sorting it; command-line input and a project structure that supports several applications from one codebase; scheduling activities with the time package; and finally writing and testing a WebSockets server. The ordering is itself the argument — each item presupposes the last, moving outward from a bare request handler to persistence, to packaging and input, to time, and ending with the hardest thing to test. The chapter closes on that WebSockets entry, functioning as a doorway rather than a destination: it has given the reader a premise, a method and an itinerary, and nothing more, leaving the actual work to the chapters it introduces.
Key claims, with the lines that support them
This part of the book is mostly about discovering what Go's bundled packages can already do, not about piling on more language features.
The author says new concepts will appear only to help write good code, while the bulk of the material is what the standard library makes possible — and the chapter list bears this out, since every listed topic names a library capability: serving HTTP requests, returning JSON and routing, reading and writing to disk with sorting, command-line input, the time package for scheduling, and WebSockets.
“New concepts will be introduced to help facilitate writing great code but most of the new material will be learning what can be accomplished from Go's standard library.”
Build an applicationRead in context →
The application is not designed once up front; it expands chapter after chapter as the stated requirements change.
The author frames each chapter as iterating on the one before, with functionality added as a product owner dictates, and the roadmap shows that accumulation concretely: it begins with a server that listens and replies, then layers on JSON responses and routing, persistence to disk, support for several programs from one codebase plus command-line input, scheduled activity, and finally WebSockets.
“Each chapter will iterate on the previous one, expanding the application's functionality as our product owner dictates.”
Build an applicationRead in context →
The promised payoff is a working method, not just a finished program.
The author states the goal as the reader ending up with a firm grasp of how to write an application in Go iteratively and backed by tests — which follows from the structure just described, since the repetition of extend-and-test across chapters is what the reader is meant to carry away.
“Each chapter will iterate on the previous one, expanding the application's functionality as our product owner dictates.”
Build an applicationRead in context →
How it works
The 2 mechanisms the chapter’s argument runs on.
Mechanism 1
Why one app grown step by step teaches iteration
Rather than starting fresh each time, every chapter extends the same program while an imagined product owner keeps asking for more. Because the reader repeatedly has to fit new demands onto code that already exists and is covered by tests, the habit of growing software in small test-backed steps is what sticks by the end.
Mechanism 2
Fundamentals first, then library breadth
The author assumes the earlier fundamentals material has already settled the language itself and the testing discipline. With that groundwork in place, each new requirement can be met by reaching for a packaged capability — an HTTP server, JSON encoding, file IO, scheduling — so the learning shifts from syntax to knowing what ships with Go.
Common misreadings
After the fundamentals, the application section must be where the harder or more advanced parts of the Go language get taught.
The author treats the language itself as largely settled by the fundamentals section and positions the new concepts here as secondary aids to writing good code; the main learning target is the set of packages Go already ships with.
“New concepts will be introduced to help facilitate writing great code but most of the new material will be learning what can be accomplished from Go's standard library.”
Build an applicationRead in context →
Each chapter in this section is a separate self-contained exercise you can pick up in any order.
They form a single continuous build: every chapter iterates on the previous one and adds functionality to the same application, so the ordering from HTTP server through to WebSockets is cumulative rather than a menu.
“Each chapter will iterate on the previous one, expanding the application's functionality as our product owner dictates.”
Build an applicationRead in context →
Key concepts in Build an application
- Iterative application development
- Growing one program step by step, each chapter adding functionality on top of the last, with tests underneath.
- Where it applies: Described here as a learning structure, not a formal methodology.
- Go standard library
- The packages shipped with Go that supply most of what the application needs.
- Where it applies: New language concepts still appear, but they are secondary to library learning.
0 more concepts
Reader’s notes on Build an applicationAI reading companion
“as our product owner dictates”
Read in context →A quiet admission that the curriculum is driven by changing requirements, not a clean spec. That's the real pedagogy here — the chapters iterate because requirements do.
“most of the new material will be learning what can be accomplished from Go's standard library”
Read in context →The strongest claim in the whole page, and it's dropped almost apologetically. Learning a language's stdlib is learning the language's culture.
“you have a solid grounding of a majority of Go's language features”
Read in context →"Hopefully digested" plus "a majority" — carefully hedged. Honest, but it leaves the reader unsure whether gaps will bite them in the next section.
3 more notes
“JSON, routing and embedding”
Read in context →Embedding smuggled in beside two HTTP chores. It's the one genuinely Go-specific idea in that list, and it gets third billing.
“Command line & project structure”
Read in context →The only entry about the shape of code rather than a feature. Telling that it arrives mid-course — structure is earned, not declared up front.
“learn how to write and test a server that uses WebSockets”
Read in context →Note the phrasing: not "use WebSockets" but "write and test." The book keeps the test clause attached even to the flashiest topic. Consistent to the end.
A reader’s takeAI reading companion
This is a signpost page, not a chapter, and its interest lies almost entirely in what it reveals about the book's method rather than any content. Three moves stand out. First, the fiction of a "product owner" who "dictates" — iteration is motivated externally, which is both honest about real work and a convenient excuse for not designing the app up front. Second, the explicit demotion of language features in favor of the standard library: the author is betting that competence in Go is mostly fluency with net/http, encoding/json, io, time — a defensible bet, and a quietly opinionated one. Third, the sequencing: HTTP server, then JSON and routing, then persistence, then project structure, then time, then WebSockets.
Structure appears fifth, after the code has had time to get messy — a deliberate refusal to teach layout before there is anything to lay out. The persistent hedging ("hopefully digested", "a majority of", "should have a strong grasp") is the one weak note; it's a reader-friendly humility that also means the page never commits to what you must already know. What I'd want from a table of contents like this is a sentence on what the application actually is — the chapters are listed by technique, never by product, which is a small irony for a section framed around a product owner.
Read Build an application with these cards beside each passage.
Open in FeedLitraBuild an application: HTTP Server
How does the next segment spy on RecordWin to verify stores?
How Build an application unfoldsAI-assisted
This chapter opens by refusing to design anything. The goal — an HTTP server that tracks players' wins — is stated, and then the first test asks only for a score to come back from a GET request. The answer is hard-coded: the handler writes a fixed number and the test goes green. This is deliberate theatre, because the second test immediately breaks the pretence by asking for a different player's score. Now the constant cannot survive, and the handler is forced to read the request URL and strip the path prefix to learn who is being asked about. The author concedes the parsing is not robust, but it is enough for now. The lesson being planted is that the next design move was not foreseen; it was squeezed out by a test the previous code could not satisfy.
From there the chapter develops by extracting seams. The score lookup is pulled into its own function, and once it stands alone the gap in the design becomes visible: that function wants to be someone else's responsibility. So GetPlayerScore becomes a method on a PlayerStore interface, and the handler becomes PlayerServer, a struct holding a store and implementing ServeHTTP. The crucial point is the direction of discovery — the interface is not drawn up first and then implemented. The handler's own tests state what the handler needs from storage, and that statement of need is the method set. A canned stub store, pre-filled with scores, feeds the GET tests, which is what lets read be built before write exists at all. The 404 for unknown players arrives next and delivers a small, sharp lesson in its own right: writing the laziest possible code — returning 404 on every response — still left the suite green, which exposed that no test had ever asserted a 200 for a player who does exist. The tests were incomplete, and only the laziness revealed it.
The turn comes with POST. Routing splits into showScore and processWin, and recording a win presents a chicken-and-egg problem: GET needs stored data, POST needs a way to read it back. The chapter breaks the deadlock with a second kind of double — a spy that simply counts calls to RecordWin. Saving is verified by observed calls, not by later retrieval, so the write half no longer waits on the read half. Adding RecordWin to PlayerStore then stops main from compiling, and the author treats that broken state as acceptable rather than alarming: the handler has already told him the correct contract, and the compiler will enumerate every concrete store that must catch up. Along the way the player name, extracted twice, is lifted into ServeHTTP once.
The chapter closes by zooming out. An integration test drives the real PlayerServer against an InMemoryPlayerStore: three recorded wins, then one score request, which returns "123" instead of "3" — concatenation where accumulation was wanted — and that failure drives the store toward a map keyed by player name. The author's framing of this test is careful and double-edged. It buys confidence in the whole flow without unit-testing a component destined to be thrown away, and it keeps that value when a real database replaces the in-memory store. But it only proves the handler and store work together; it is harder to write, slower, and harder to localise when it breaks. That is precisely why the fast, focused handler tests remain underneath it rather than being retired.
The closing lessons tie the chapter's method to its subject. http.Handler is the abstraction that made all of this testable, and dependency injection is what let a stub, a spy, and a real store occupy the same slot. Then the methodological claim: a failing compilation is the same emergency as a failing test. Both mean you are flying blind, and every change made while red is unverified — pile on a stub store and a new interface method at once and you have several tangled problems instead of one. Escape red with the least code possible, then refactor with passing tests at your back. The discipline forces small tests, small tests force small changes, and small changes are how complicated systems stay workable.
Key claims, with the lines that support them
Test doubles are what break the deadlock where GET needs stored data and POST needs a way to read it back.
The author serves GET from a pre-filled fake store and checks POST by watching its calls to the store, so storing is never verified through retrieval and the two features stop blocking each other.
“For POST we can spy on its calls to PlayerStore to make sure it stores players correctly.”
HTTP Server › Chicken and eggRead in context →
The next design step was uncovered by tests and refactoring rather than planned in advance.
Adding a second player's score made the hard-coded answer fail, which forced the handler to read the request URL and strip the prefix—an approach the author admits is not robust but good enough for now. Pulling that score lookup out into its own function then made the seam obvious, and from there PlayerServer became a struct holding a store and implementing ServeHTTP.
“This test has forced us to actually look at the request's URL and make a decision.”
HTTP Server › Write enough code to make it passRead in context →
Writing only the laziest code that passes is useful because it exposes assertions the tests forgot to make.
Returning a 404 on every single response still kept the suite green, which revealed that no test had ever asserted a 200 for players who do exist.
“By doing the bare minimum to make the tests pass it can highlight gaps in your tests.”
HTTP Server › Write enough code to make it passRead in context →
3 more claims
Interfaces should be discovered while testing the handler instead of designed up front.
Following on from the earlier extraction, the author grows the store interface only when the handler's test demands RecordWin; main then stops compiling, and he treats that broken state as acceptable because the handler has already told him the right contract and the compiler lists what to fix.
“We need to update PlayerServer's idea of what a PlayerStore is by changing the interface if we're going to be able to call RecordWin.”
HTTP Server › Write enough code to make it passRead in context →
A whole-flow test can give confidence in the system without unit-testing a throwaway component, and it keeps its value once a real database replaces that component.
Three recorded wins followed by one score request returned the body "123" instead of "3", which drove the store toward a map keyed by player. The author notes such tests only check the pair end to end, and warns they are harder to write, slower, and harder to diagnose.
“This will let us get to our goal of being confident our application is working, without having to directly test InMemoryPlayerStore.”
HTTP Server › RefactorRead in context →
Failing compilation counts as the same emergency as failing tests: get out with minimal code, then clean up.
Closing the chapter, the author argues that long spells in red accumulate untested changes and rabbit holes, whereas this discipline forces small tests and therefore small changes, which is how complicated systems stay workable.
“You need to treat having failing compilation or failing tests as a red situation that you need to get out of as soon as you can.”
HTTP Server › Commit sins, then refactor (and then commit to source control)Read in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Why short red phases stay safe
While code fails to compile or tests fail, every extra change is unverified, so piling on new pieces such as stub stores multiplies the problems you must untangle at once. Escaping red with the least code first keeps each step small and keeps the refactor that follows backed by passing tests.
Mechanism 2
Stub and spy split read from write
GET and POST each need the other to be useful, so the author replaces the store with test-only versions: a canned store feeds scores to the GET handler while a recording double counts RecordWin calls for POST. Because saving is checked by observed calls rather than by later retrieval, the two halves can be built independently.
Mechanism 3
Handler tests surface the interface
Writing tests against the handler forces you to state exactly what it needs from storage, and that need becomes the method set. Adding RecordWin to the interface then breaks compilation elsewhere, and the compiler itself points out which concrete store still has to catch up.
Common misreadings
You should settle the storage interface first and only then write the handler against it.
The author arrives at the interface the other way round—by testing the handler until it states what it needs—and he is content to leave the wider application temporarily uncompilable, letting the compiler name each place that must be brought into line.
“We need to update PlayerServer's idea of what a PlayerStore is by changing the interface if we're going to be able to call RecordWin.”
HTTP Server › Write enough code to make it passRead in context →
Once the end-to-end test passes, it is the test worth keeping and the small handler tests are redundant.
That test only shows the handler and store work together; the author flags it as harder to write, slower, and harder to localise when it breaks, which is why the fast focused tests stay underneath it.
“This will let us get to our goal of being confident our application is working, without having to directly test InMemoryPlayerStore.”
HTTP Server › RefactorRead in context →
Key concepts in Build an application
- TDD cycle
- Write a failing test, do the least work to pass, then clean up safely.
- Where it applies: The 'commit sins' license only holds because the refactor step is backed by tests.
- Interface
- A named set of methods a type can satisfy, here PlayerStore for score lookup.
- Where it applies: This chapter extracts it only after a concrete function made the seam obvious.
- Mocking
- Replacing a real dependency with a simple test-only version.
- Where it applies: A map-backed stub suffices for reads; spying is needed to check writes.
- httptest
- Go's package giving a ready-made response spy for handler tests.
- Where it applies: It records responses in-process; it does not exercise a real listening port.
- Handler
- Any type with ServeHTTP; HandlerFunc adapts a plain function into one.
- Where it applies: This chapter uses HandlerFunc only while the server holds no state.
- Spy
- A test double that records the calls made to it so the test can assert on them.
- Where it applies: Useful for side effects like RecordWin; says nothing about real persistence.
4 more concepts
- Acceptance test
- Here an integration test wiring PlayerServer to InMemoryPlayerStore to check the whole flow works.
- Where it applies: Harder to write, slower, and failures are harder to localize.
- Test Pyramid
- Guidance to keep many fast unit tests under fewer slow integration tests.
- Where it applies: Mentioned as further reading, not derived in this chapter.
- Dependency Injection
- Passing a store into PlayerServer so the handler can be developed and tested without real storage.
- Where it applies: Needs an interface the handler owns; swapping implementations must keep the same contract.
- Mutex
- A lock added so concurrent requests don't read and write the score map at once.
- Where it applies: This chapter only points to the sync chapter for details.
Reader’s notes on Build an applicationAI reading companion
“Make the test work quickly, committing whatever sins necessary in process.”
Read in context →The permission to sin only works because the refactor step is non-negotiable. Plenty of teams quote the first half and skip the second — that's how you get the codebase everyone blames TDD for.
“so we're just trying to break the constant for now”
Read in context →"Break the constant" is the real honest description of the second test. The test isn't about Floyd at all; it's a crowbar aimed at hard-coding.
“the next logical step actually seems to be about routing”
Read in context →The nicest moment so far: the test contradicted the author's own plan. He'd announced mocking and PlayerStore up front, then the smallest step turned out to be URL parsing instead.
12 more notes
“I have done the bare minimum (knowing it is not correct), which is write a StatusNotFound on all responses but all our tests are passing!”
Read in context →He's using deliberately wrong code as a detector for missing assertions — the test suite gets tested. That inversion is worth more than the 404 feature itself.
“if score == 0 { w.WriteHeader(http.StatusNotFound”
Read in context →A real player with zero wins now 404s — and the body still gets written after the header. The tests pass only because the stub's map conflates "absent" with "zero". A bug the chapter builds in and doesn't flag here.
“You should be in the habit of prioritising having code that compiles and then code that passes the tests.”
Read in context →Underrated discipline: the compiler is a cheaper feedback loop than the test runner, so empty the queue of type errors before you reason about behaviour.
“p.store.RecordWin("Bob")”
Read in context →Hard-coding "Bob" exposes that asserting only on call count is a weak spy. The fake answer is the argument for the next test — sinning as a diagnostic again.
“Even though our tests are passing we don't really have working software.”
Read in context →The most honest line in the chapter. Green tests as local anesthetic — the author names the failure mode of his own method before anyone else can.
“by focusing on our handler we have identified the interface that we need, rather than trying to design it up-front”
Read in context →The whole argument in one clause: interfaces as a discovered residue of use, not a plan. Convincing here because the handler had a real caller's need — less convincing where nobody calls you yet.
“without having to directly test InMemoryPlayerStore”
Read in context →Note the move: a throwaway component gets covered sideways, and the test survives the component it was written around. That reusability is the real payoff, not the coverage.
“I am going to take some liberties here and write more code than you may be comfortable with without writing a test.”
Read in context →A teacher admitting he's about to break the rule he's teaching — and more usefully, naming the escape hatch if it goes wrong. TDD as a dial, not a vow.
“response body is wrong, got '123' want '3'”
Read in context →'123' is a beautiful bug — the stub appended instead of counting, and the failure message tells you exactly which fiction you were living in. A good test failure is a sentence, not a stack trace.
“Commit sins, then refactor (and then commit to source control)”
Read in context →The pun carries the discipline: sin is permitted only because confession is scheduled. Most codebases do the first half.
“You need to treat having failing compilation or failing tests as a red situation that you need to get out of as soon as you can”
Read in context →Red reframed as debt accruing interest, not a stage to inhabit. The reason given is cognitive, not moral — compounding problems, not bad practice.
“By adding mutexes, we enforce concurrency safety”
Read in context →Odd that the one bug the running server would actually hit gets a sentence and a link — no test, no red. The chapter's discipline quietly lapses right where concurrency makes testing hard.
A reader’s takeAI reading companion
Read as one piece, this chapter is less a tutorial on HTTP servers than a demonstration of how a design can be discovered rather than planned. It starts from a hard-coded '20', breaks that constant with a second player, and only then — against the author's own announced plan to introduce mocking and a store — finds that the smallest honest next step is URL parsing. That moment is the chapter's thesis in miniature: the tests, not the architect, decide the order of work. From there PlayerStore emerges as a residue of what the handler actually needs, PlayerServer becomes a struct implementing http.Handler, 404 handling arrives, POST splits routing into showScore and processWin, a spy drives RecordWin into the interface, and an integration test finally ties a real InMemoryPlayerStore to the server. The arc is genuinely cumulative: each component exists because something downstream demanded it.
What I most respect is the chapter's willingness to use wrong code as an instrument. Writing StatusNotFound on every response to see whether the suite notices is a test of the tests; hard-coding RecordWin("Bob") exposes that a spy counting calls without checking arguments is barely a spy; the '123' failure message is a small masterpiece of diagnosis. 'Commit sins, then refactor' only works because confession is scheduled — and the author knows it, which is why he also says, flatly, that passing tests do not mean working software. Treating red as compounding debt rather than a stage to inhabit, and prioritising compilation over correctness because the compiler is the cheaper feedback loop, are the kind of operational details most TDD writing omits.
Continue reading
Where it slips, it slips in instructive ways. The zero-score 404 is a real bug built into the text and not flagged: a player with zero wins is indistinguishable from an absent one because the stub's map conflates the two, and the body is still written after the header. And concurrency — the one failure the running server will actually meet — gets a mutex, a sentence and a link, no red, no test. The discipline lapses precisely where testing is hard, which is exactly where it was supposed to prove itself.
To the reader: take the method, not the ceremony. The valuable moves here are the diagnostic sin, the deliberately broken implementation that audits your assertions, the interface extracted from a caller that already exists, and the integration test that outlives the throwaway component it was written around. Treat TDD as a dial the author himself admits to turning — and when you hit the place where tests get awkward, notice that the chapter flinched, and don't.
Read Build an application with these cards beside each passage.
Open in FeedLitraBuild an application: JSON, routing & embedding
How would sorting and persistence change the PlayerStore interface?
How Build an application unfoldsAI-assisted
The chapter picks up where the previous one left off — a small web server that records and reports how many games players have won — and introduces a fresh demand from the product owner: a /league endpoint returning every stored player as JSON. Before touching JSON at all, the author takes the smallest possible step and writes a test asserting only that GET /league returns 200. It fails with a 404, and the reason is instructive: ServeHTTP begins by trimming /players/ off the path, so the server structurally assumes every request is about one named player. The failing test, as the author frames it, isn't just reporting a bug — it's telling us the application has no concept of routing.
Development: routing, then a detour into embedding
The fix arrives in the standard library's ServeMux. The author deliberately "commits some sins" first: builds a router inside ServeHTTP, attaches two anonymous handlers (/league and /players/), and delegates to the router — ugly but green. Then comes a staged refactor, each step motivated by a specific discomfort. The anonymous functions become named methods leagueHandler and playersHandler. Building a router per request is wasteful, so routing setup moves into a new NewPlayerServer constructor that stores the mux on the struct. Finally the named router *http.ServeMux field is replaced by an unnamed http.Handler, which the constructor fills with the router — and PlayerServer's own ServeHTTP method can be deleted outright.
That last move triggers the chapter's main conceptual aside: embedding. Go has no subclassing, but a struct can borrow an embedded type's methods, so PlayerServer inherits ServeHTTP for free. The author then dwells on the cost side, which is the real lesson: embedding exposes everything public in the embedded type. Embedding the http.Handler interface exposes only ServeHTTP; embedding the concrete http.ServeMux would have let any caller register new routes on your server. The advice is to treat embedding as a public-API decision, not a convenience.
Turn: from "it responds" to "it responds with the right data"
With the structure in place, attention shifts to content. The next test decodes the response body into a []Player rather than comparing JSON strings, and the author pauses to justify that choice: string assertions are brittle, hard to debug, express the wrong intent, and amount to re-testing the standard library's encoder. He notes the pleasing symmetry of encoding/json — Encoder needs an io.Writer (which http.ResponseWriter is), Decoder needs an io.Reader (which the recorder's Body is). The handler is made to pass first with a hard-coded one-element league, then the test is tightened to demand specific stubbed players, which forces GetLeague() onto the PlayerStore interface. The compiler duly complains in five places, and the author accepts a deliberately hollow GetLeague() returning nil in InMemoryPlayerStore — minimal code to go green, with the discomfort explicitly logged as future work. A test refactor extracts newLeagueRequest, getLeagueFromResponse, and assertLeague, and a further small cycle adds the content-type: application/json header plus an assertContentType helper.
Closing: paying the parked debt, then the summary
The chapter settles its own debt. Because /league would embarrass a demo while InMemoryPlayerStore.GetLeague() still returns nil, the author extends the integration test — reusing the server-test helpers, a payoff he points to as justification for refactoring tests — and watches it fail with got [] want [{Pepper 3}]. The real implementation is four lines: range over the map, append a Player per entry. The wrap-up names the three takeaways in order of appearance: ServeMux routing (easy, fully committed to http.Handler, but with no path variables, so reach for a third-party router if parsing becomes a chore), type embedding (powerful, but only expose what's appropriate), and JSON serialization (near-trivial and configurable). The closing line sets up the sequel: persistence and sorting the league.
Key claims, with the lines that support them
Routing and dependency wiring belong in a constructor, not in the per-request handler.
The author first gets the /league test green with a router built inside ServeHTTP, then calls that arrangement odd and wasteful, and refactors it into NewPlayerServer, which assembles the ServeMux once and keeps it for every later request.
“We have moved the routing creation out of ServeHTTP and into our NewPlayerServer so this only has to be done once, not per request.”
JSON, routing & embedding › RefactorRead in context →
When embedding, the thing to weigh is what it adds to your public API, not the mechanics of embedding itself.
Embedding promotes everything public from the embedded type, so the author embeds only the narrow http.Handler interface; had he embedded the concrete http.ServeMux, callers of PlayerServer could invoke its public Handle and register routes of their own. The chapter leaves the deeper mechanics to Effective Go and reduces its own lesson to exposing only what is appropriate.
“When embedding types, really think about what impact that has on your public API.”
JSON, routing & embedding › Any downsides?Read in context →
Tests of a JSON endpoint should decode the response into the program's own types rather than compare raw JSON text.
The author lists four costs of string assertions: they break whenever the data model changes, give unhelpful diffs, express how data is encoded instead of what it is, and in effect re-test the standard library's encoder, which is already tested. His test therefore decodes the body into a slice of Player and compares that.
“Instead, we should look to parse the JSON into data structures that are relevant for us to test with.”
JSON, routing & embedding › Why not test the JSON string?Read in context →
1 more claim
The standard library's router is a fine starting point; reach for a third-party one only once its gaps actually hurt.
The wrap-up notes ServeMux lacks features such as path variables like /users/{id}, which you can parse yourself. Because the router is itself an http.Handler and the popular libraries follow that same philosophy, switching later is cheap, so the author advises looking elsewhere only when the missing features become a burden.
“You can easily parse this information yourself but you might want to consider looking at other routing libraries if it becomes a burden.”
JSON, routing & embedding › Wrapping upRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Build the router once at construction
ServeMux maps each URL path to a handler, so creating it inside ServeHTTP rebuilds the same table on every request. Moving that wiring into NewPlayerServer makes it one-time setup, and each incoming request simply delegates to the stored router.
Mechanism 2
Embedding borrows the handler method
Go has no subclassing, but a struct that embeds a type gains that type's methods. Embedding http.Handler gives PlayerServer a ServeHTTP, and assigning the router to that field makes the router answer requests, so the hand-written ServeHTTP can be deleted.
Mechanism 3
Writer and reader symmetry in JSON
json.Encoder needs an io.Writer and http.ResponseWriter is one, so a league slice can be encoded straight into the response. Decoding mirrors this: json.Decoder needs an io.Reader, which the recorded response body provides, letting tests read the same data back.
Common misreadings
The straightforward way to test a JSON endpoint is to assert the body equals an expected JSON string.
Decode the body into the domain types and compare those. String comparison is brittle under data-model changes, hard to debug, and effectively tests the standard library's encoder instead of your own data.
“Instead, we should look to parse the JSON into data structures that are relevant for us to test with.”
JSON, routing & embedding › Why not test the JSON string?Read in context →
Embedding is just a shortcut for reuse, so embedding the concrete http.ServeMux would be as good as embedding http.Handler.
Embedding a concrete type promotes all of its public methods and fields onto your type, so an embedded ServeMux would let outside callers use Handle and add routes to the server. Embedding just the http.Handler interface exposes only ServeHTTP, which is why it is the safe choice here.
“When embedding types, really think about what impact that has on your public API.”
JSON, routing & embedding › Any downsides?Read in context →
Key concepts in Build an application
- Embedding
- Putting a type inside a struct so the outer type borrows its methods, e.g. PlayerServer gaining ServeHTTP from http.Handler. Putting one type inside another so the outer type gains its behaviour directly.
- Where it applies: Embedding a concrete type exposes all its public methods and fields, so prefer embedding only the interface you want to expose. Useful, but it promotes the embedded type's methods into your public API, so expose only what is appropriate.
- ServeMux
- Go's built-in request multiplexer that maps URL paths to http.Handlers.
- Where it applies: Should be built once at construction, not per request, since per-request setup is odd and inefficient.
- io.Writer
- The standard writing interface; http.ResponseWriter implements it, so a json.Encoder can write straight to the response.
- Where it applies: Symmetric counterpart is io.Reader, needed by json.Decoder.
- http.Handler router
- The standard library routing type that maps routes to handlers and is itself a handler.
- Where it applies: It lacks path variables such as /users/{id}; you parse them yourself or move to a routing library.
- JSON deserializing and serializing
- Turning Go data into JSON and back using the standard library.
- Where it applies: Defaults cover most cases, and the transformations can be customised when needed.
0 more concepts
Reader’s notes on Build an applicationAI reading companion
“Let's commit some sins and get the tests passing in the quickest way we can”
Read in context →The honesty here is the pedagogy. Creating a router per request is genuinely bad code, and naming it a sin up front is what makes the later refactor feel earned rather than arbitrary.
“notice how ServeMux is also an http.Handler?”
Read in context →This parenthetical is the whole chapter in miniature — the router is a handler, so routers compose with handlers. Standard library self-similarity doing heavy lifting.
“we embedded just the interface that we wanted to expose (http.Handler)”
Read in context →Embedding the interface rather than the concrete type is the sharpest single decision in the chapter. ServeMux would have worked identically and leaked Handle() to every caller.
5 more notes
“Re-testing the standard library. There is no need”
Read in context →Four reasons given and this is the one that generalizes beyond JSON. Most brittle tests I've seen are really assertions about someone else's serializer.
“json.NewEncoder(w).Encode(leagueTable) w.WriteHeader(http.StatusOK)”
Read in context →This is a latent bug the chapter never names: writing the body implicitly sends 200, so the WriteHeader after it is a no-op. It silently disappears two refactors later when the content-type fix rewrites the function — the test never caught it because 200 was the wanted status anyway.
“live with the uncomfortable feeling of an incomplete implementation”
Read in context →Returning nil to appease the compiler is the move most people would quietly skip past. Admitting the discomfort is what later makes the integration test feel like a debt being paid rather than a new feature.
“showing the importance of refactoring tests”
Read in context →The helpers written for the unit tests are reused wholesale by the integration test. Test code treated as real code pays off within a single chapter — rare to see that demonstrated rather than asserted.
“league = append(league, Player{name, wins”
Read in context →Map iteration order is random in Go, so this league is unordered. The integration test only passes because there is exactly one player — add a second and it flakes. The next chapter's "sorting" is really a bug fix wearing a feature's clothes.
A reader’s takeAI reading companion
A deceptively tidy chapter whose real subject is not JSON at all — JSON is the easy part, handled in four lines by the standard library. The actual lesson is twofold: ServeMux is an http.Handler, and interfaces are the right thing to embed. Those two facts combine into the elegant final form where PlayerServer is a handler by borrowing one, and the author's warning about embedding ServeMux instead of http.Handler is the most transferable advice in the text. The TDD method shows itself honestly, including its blind spots: two real defects slip through green tests here. The WriteHeader-after-Encode ordering bug is silently erased by a later refactor rather than diagnosed, and GetLeague's map iteration makes the league order nondeterministic — invisible only because the integration test has a single player. Both are cases where the test was large enough to pass and too small to reveal, which is a sharper teaching about test-driven development than anything the wrap-up claims. The section on why not to assert against JSON strings is the strongest standalone argument: test the data, not the encoding, and don't re-test code you don't own.
Read Build an application with these cards beside each passage.
Open in FeedLitraBuild an application: IO and sorting
How does the tape type guarantee old bytes are truncated?
How Build an application unfoldsAI-assisted
The opening: a new requirement, met with the narrowest interface that works. The chapter picks up a player store that has lived purely in memory and asks it to survive a restart, so the data has to go to a JSON file. Rather than reaching for a filename and os.Open, the author starts from the standard library's IO abstractions and takes the smallest one that could possibly do the job: io.Reader. The store is handed something it can read from, decodes the JSON into a slice of players, and the first test passes. A League type is introduced to give that slice a name and a home for behaviour, with a Find method so callers stop hand-rolling loops over players.
The development: the design is pushed wider, one failure at a time. The reason the chapter keeps changing the interface is a specific mechanical fact, not taste. Reading from a reader consumes it; once the decoder has walked to the end of the file, the next read begins at EOF and sees nothing at all. So the moment a test asks for a second read, the plain Reader is no longer enough and the parameter widens to io.ReadSeeker, with a Seek(0, io.SeekStart) call to rewind before parsing again. The same pressure applies again when wins must be recorded: writing requires io.ReadWriteSeeker. A second, subtler trap appears here. Ranging over a slice yields the index plus a copy of each element, so incrementing Wins on the loop variable mutates a value that is about to be discarded; the write has to go through league[i] or a pointer into the slice, which is exactly why Find is made to return a pointer. The author then notices that re-parsing the file on every request is wasteful when the store is the thing that owns this state, and moves the seek-and-decode into the constructor: the league is read once at start-up, kept in a field, and the disk is touched only when something actually changes.
The turn: the leftover-bytes bug, and a private type to contain it. Persisting a win exposes the chapter's nastiest defect. Seeking to position zero and writing overwrites only as many bytes as you actually write — anything longer in the old file simply survives. Write "abc" over "12345" and the file reads abc45, which is corrupt JSON. The fix is not a one-line patch but a separation of concerns: what gets written is pulled apart from how it gets written. A small, unexported tape type takes over the mechanics — truncate the file, then seek to the start, then write — so that this risky behaviour can be exercised by a test of its own instead of staying tangled up with JSON encoding. The author is candid that testing an unexported type is a compromise, and justifies it narrowly: tests exist to buy confidence, this is the only practical way to get it here, and it is a single test that costs nothing to delete if the design moves on. In the meantime it documents the truncate-then-write requirement for whoever maintains the code. The lesson is framed as a deliberate exception, not a licence — tests against internals do couple you to the implementation and make refactoring harder.
The close: paying off the debt, and the reward. With Truncate in place, the second half tidies what the earlier haste left behind. The json.Encoder is created once and cached rather than rebuilt per write. Errors that had been silently discarded are surfaced and returned from the constructor — and doing so immediately reveals a crash that had been hiding in plain sight. A brand-new, zero-length database file is not valid JSON, so decoding it genuinely fails; the only reason this ever looked harmless was that the parse error was being thrown away. The remedy is explicit initialisation: use file.Stat to detect a zero-size file, write an empty [] into it, and seek back to the start so the decode succeeds. Having cleaned all this up, the chapter ends by cashing in. The last requirement — serve the league sorted by wins — becomes almost trivial: a single sort.Slice call in the right place, with no new plumbing. That contrast is the chapter's closing argument, and the reason the sorting feature is saved for last: the refactoring and error handling were not detours from the feature work, they were what made the final feature cheap.
Key claims, with the lines that support them
The store owns the league's state, so it should load the file once when it is built and touch the disk only when something changes.
The author moves parsing into the constructor: it seeks, decodes the league once, and keeps it in a field, after which reads just serve the in-memory copy — paying the disk cost a single time at start-up instead of on every request.
“it should only need to read the file when the program starts up and only need to update the file when data changes.”
IO and sorting › More refactoring and performance concernsRead in context →
Following on from the leftover-bytes bug, the fix is to split what gets written from how it gets written.
The author refactors the rewind-and-overwrite behaviour into its own small type so that concern can be exercised directly in a test, rather than being tangled up with producing the JSON.
“As we thought! It writes the data we want, but leaves the rest of the original data remaining.”
IO and sorting › Try to run the testRead in context →
Reaching into a private type with a test is acceptable when that is the only practical way to be confident about a risky behaviour.
The author's justification is that tests exist to give confidence, and it is only one test: if the design later changes, deleting it costs nothing, while in the meantime it records the edit/delete requirement for whoever maintains the code — accepting that such tests do couple you to the implementation.
“However, we must not forget that tests should give us confidence.”
IO and sorting › On testing private typesRead in context →
1 more claim
Errors that are quietly discarded turn into real bugs, so the constructor has to return them and cope with a brand-new empty file.
The empty-file crash was invisible precisely because the parse error was being swallowed; once errors are surfaced, file.Stat is used to spot a zero-length file, write an empty JSON array into it, and seek back to the start so decoding succeeds.
“We cannot parse the league because the file is empty. We weren't getting errors before because we always just ignored them.”
IO and sorting › Error handlingRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Why a reader runs dry
Reading the file consumes it up to the end, so the next read starts at EOF and finds nothing. That is why the store cannot take a plain Reader: it needs a ReadSeeker and must call Seek(0, io.SeekStart) to rewind before parsing again.
Mechanism 2
Range hands you a copy
Ranging over a slice gives the index plus a copy of each element, so bumping Wins on the loop variable changes only that throwaway copy. To record a win you must write through league[i] or a pointer into the slice, which is why Find returns a pointer.
Mechanism 3
Short writes leave leftovers
Seeking to the start and writing only overwrites as many bytes as you write; anything longer in the old file survives. Writing "abc" over "12345" leaves 'abc45', so the file must be truncated first and only then seeked and rewritten.
Common misreadings
An empty database file is harmless — decoding it should just give you an empty league.
An empty file is not valid JSON, so the decode genuinely fails; the only reason this looked fine was that the error was being ignored. The code has to detect the zero-size file, seed it with "[]", and rewind before parsing.
“We cannot parse the league because the file is empty. We weren't getting errors before because we always just ignored them.”
IO and sorting › Error handlingRead in context →
Since the chapter tests an unexported type, writing tests against internals is generally fine.
The author treats it as a deliberate exception: such tests tie you to the implementation and make refactoring harder, so it is defensible only when nothing else gives you confidence in the behaviour, and only because it is a single test that is cheap to throw away.
“However, we must not forget that tests should give us confidence.”
IO and sorting › On testing private typesRead in context →
Key concepts in Build an application
- io.Writer
- Standard interface for writing bytes; here paired with reading and seeking in one file-shaped interface.
- Where it applies: strings.Reader cannot satisfy it because it has no Write method.
- Find
- A method on the new League type that returns a pointer to the matching player, or nil.
- Where it applies: Returns nil when the player is absent, so callers must handle the new-player case.
- Type declaration from existing type
- Naming []Player as League so useful methods can hang off it.
- Where it applies: Changing the PlayerStore return type forces small compile fixes across callers.
- t.Helper()
- Marks test helpers like createTempFile and assertScoreEquals so failures point at the caller.
- Where it applies: Only affects reported line numbers, not behaviour.
- Interface
- A set of method requirements; this chapter argues not to invent one when only *os.File will ever be passed.
- Where it applies: Useful for polymorphism and mocking; pointless when a single concrete type is the only realistic argument.
- Error wrapping with %w
- This chapter wraps with %v but insists errors carry context about what you were doing.
- Where it applies: Context matters for operators; bare
return errloses the trail.
1 more concept
- sort.Slice
- Sorts a slice in place using a less function you supply.
- Where it applies: This chapter puts sorting in PlayerStore because a real database would use ORDER BY.
Reader’s notes on Build an applicationAI reading companion
“She is also not pleased that we didn't interpret the /league endpoint should return the players ordered by the number of wins!”
Read in context →The product owner as a recurring narrative device — she's not a character, she's a pressure source that justifies the next refactor. Cheap but effective pedagogy.
“The problem is our Reader has reached the end so there is nothing more to read.”
Read in context →This is the real lesson of the chapter: an interface isn't just a method set, it carries hidden state. Reader looks pure until you call it twice.
“Happily for us strings.NewReader that we used in our test also implements ReadSeeker so we didn't have to make any other changes.”
Read in context →Lucky, and the author knows it. Widen the required interface and your test double usually breaks — this time it didn't. Worth noticing as luck, not method.
11 more notes
“I don't think there's an especially wrong answer here, but by choosing to use a third party library I would have to explain dependency management!”
Read in context →Rare honesty: the design decision is driven by the book's syllabus, not the code. I'd rather he say that than dress it as engineering judgement.
“Changing the Wins value of a copy won't have any effect on the league slice that we iterate on.”
Read in context →The one place where the empty-looking implementation and the "working" one differ by a single character. Range copying is the bug Go programmers rediscover forever.
“We should not have to do that because FileSystemStore is entirely responsible for the state of the league”
Read in context →Note what the cache buys and what it costs: the file is no longer the source of truth, memory is. Two writers to that file and the whole design collapses silently.
“In our current case, this is impossible. We never edit or delete scores so the data can only get bigger.”
Read in context →He fixes a bug he's just proven can't occur yet — then invents a test to force it into view. That's the most mature move in the chapter: write the test that makes the latent bug observable.
“tape.Write seeks to the start of the file on every call, so it's worth pausing to check this combination is actually safe”
Read in context →A strange seam in the prose — suddenly a careful invariant proof about Encode calling Write exactly once. Correct, but it rests on an undocumented implementation detail of the stdlib, not a contract. Truncate+Seek-per-Write is a landmine aimed at the next person who swaps the encoder.
“Finally, it's just one test! If we decide to change the way it works it won't be a disaster to just delete the test”
Read in context →The honest defense isn't principle but cheapness — a reversible decision. That's the real argument, and it's stronger than any appeal to purity.
“But what is this really giving us? Bear in mind we are not mocking and it is unrealistic for a file system store to take any type other than an *os.File”
Read in context →Nice refusal. He writes the clever interface out in full and then declines it — showing the temptation is the whole lesson.
“People on the internet jokingly say that most Go code is:”
Read in context →A joke used as a teaching wedge: the meme is the anti-pattern. Bare return err throws away the only thing an operator needs — where it happened.
“We cannot parse the league because the file is empty. We weren't getting errors before because we always just ignored them.”
Read in context →The best line in the segment. Handling errors didn't create a bug; it revealed one that was always there, silently passing.
“If we were using a "real" database we would use things like ORDER BY so the sorting is super fast. For that reason, it feels like implementations of PlayerStore should be responsible.”
Read in context →Placement argued from a database we don't have — designing for the store we'll migrate to. Though sorting on every read, in-place, is a lazy shortcut he doesn't flag.
“In order to be able to break the rules you must understand them first.”
Read in context →The guitar analogy saves this from being a license. Rule-breaking as literacy, not as taste — the distinction most people skip.
A reader’s takeAI reading companion
This chapter is the first in the book where persistence arrives, and it arrives the way it does in real projects: not as a design, but as a sequence of concessions. The store moves from a map to a JSON file, and every step is forced by something breaking rather than chosen in advance. The io interface ladder — Reader, then ReadSeeker, then ReadWriteSeeker, then a refusal to climb further — is the spine of the whole thing, and it teaches something more valuable than 'accept interfaces': an interface is not just a method set, it carries hidden state. Reader looks pure until you call it twice, the second read returns nothing, and suddenly you need Seek. That single failure generates most of the chapter. The second half is the mess that follows: writing shorter JSON over longer JSON leaves trailing garbage, so a tape type appears to Truncate and rewind; the encoder gets cached; errors stop being ignored; and the league finally gets sorted because the product owner — a narrative pressure source rather than a character, cheap but effective — points out nobody asked for unordered players.
What I most respect is the chapter's willingness to show work it then throws away. The clever generic file-system interface is written out in full and declined, on the grounds that nothing but *os.File will ever be passed; the temptation is the lesson. The corruption bug is fixed after the author proves it cannot yet occur, and then a test is invented to force the latent failure into view — the most mature move here, and the one I'd most want a reader to copy. The defense of the trailing-newline test is cheapness, not principle: it's one test, deleting it later costs nothing. That reversibility argument is stronger than any appeal to purity, and the book is better for saying so. Likewise the admission that avoiding a third-party library was a syllabus decision, not engineering judgement — rare honesty that I'd take over a dressed-up rationale any day.
Continue reading
Where I'd push back is on what the design quietly buys and what it quietly costs. Caching the league in the constructor makes memory the source of truth and the file a mere projection; two writers to that file and the whole thing collapses silently, with no error anywhere. The Truncate-plus-Seek-on-every-Write combination is provably safe only because json.Encoder happens to call Write once — an implementation detail, not a contract, and a landmine aimed squarely at whoever swaps the encoder. Sorting in place on every read is a shortcut the text doesn't flag, and the placement argument ('a real database would use ORDER BY, so the store should own sorting') designs for a database we don't have. Handling errors, meanwhile, didn't introduce the empty-file bug; it revealed one that had always been passing silently — the best single line in the chapter, and the clearest case for why bare return err and ignored returns are the same failure wearing different clothes. The meme about Go being nothing but error checks is used as a wedge: the joke is the anti-pattern, because an unwrapped error throws away the only thing an operator needs, which is where it happened.
To the reader: treat this chapter as a record of a real refactor, not a blueprint to copy. Follow the method — let the test force the next interface, widen reluctantly, write the clever abstraction down and then refuse it, and build the test that makes a bug you can't yet trigger visible. But read the final design sceptically, because the author's own closing note applies to him too: you have to understand the rules before you break them, and a few of the shortcuts here are breaks he doesn't announce. The ability to spot which is which is exactly what the chapter is trying to give you.
Read Build an application with these cards beside each passage.
Open in FeedLitraBuild an application: Command line and project structure
When is exposing test stubs as public API a mistake?
How Build an application unfoldsAI-assisted
The chapter opens with a product-owner pivot: the existing poker project, which so far builds only an HTTP server, now also needs a command line application that records a win when a user types something like "Ruth wins" — and crucially, both applications must share one database so the league stays consistent. The author grounds this by recalling the PlayerStore interface (score lookup, win recording, league retrieval) and the FileSystemPlayerStore implementation built in the previous chapter, which is the piece worth re-using.
Before writing any new feature, the chapter detours into project restructuring, since Go requires every binary's entry point to sit in package main. All the domain code is pulled out into a package named poker at the project root, and the existing main.go moves into cmd/webserver/, following the common convention that cmd holds the main packages while importable library code lives at the root. A tree listing illustrates the result, and main.go now imports the library by its full repository path and calls things like poker.NewFileSystemPlayerStore. The author pauses on a practical wrinkle: dbFileName is a relative path, so the JSON database is resolved against the working directory — meaning the two binaries will silently use separate files unless you run them from the same place, which the "Final checks" section suggests doing by building both binaries and running them from the project root.
The development phase is classic TDD in small increments. A walking skeleton cmd/cli/main.go prints a greeting, then a root-level CLI_test.go asserts that a not-yet-existent CLI type's PlayPoker records a win against the shared StubPlayerStore. The first pass hardcodes a winner; a second iteration adds an io.Reader dependency and feeds input via strings.NewReader, again satisfied by a hardcoded name; a refactor extracts an assertPlayerWin helper shared with the server tests; and only once two subtests demand different winners does real code appear — a bufio.Scanner reading a line, with extractWinner stripping the " wins" suffix.
The turn comes when the author wires the CLI into main. Constructing poker.CLI{store, os.Stdin} from package main fails, because the struct's fields are unexported: the tests could reach them only by living in the same package. This is framed as a design lesson — the dependencies were rightly kept private, but no constructor was provided — and prompts a broader argument for naming test packages foo_test, so tests consume only the exported API exactly as real importers would. Switching CLI_test.go to poker_test immediately breaks the stub and helpers, raising the subjective question of whether test doubles should be public. Citing HashiCorp's practice (via a Mitchell Hashimoto talk), the author opts to export them in a new testing.go, sparing future users from reinventing a stub. A NewCLI constructor resolves the original error, and since it wraps the reader in a scanner at construction time, PlayPoker simplifies into a readLine plus a record call.
A final refactor notices that both mains duplicate the open-file-then-build-store dance, so FileSystemPlayerStoreFromFile encapsulates it, returning the store, a closing function, and an error; both applications shrink to near-identical setup code, which the author reads as validation of the design. The wrap-up ties the threads together: restructuring into separate cmd folders enabled two binaries from shared domain code; the unexported-field collision shows the value of integrating in thin slices and of the mypackage_test discipline; os.Stdin is easy to work with because it satisfies io.Reader, with bufio.Scanner handling line-by-line reading; and simple abstractions plus a publicly exposed stub made the new application almost effortless to build and test.
Key claims, with the lines that support them
Elaborate package structure should be added only when the project actually needs it.
The author notes that keeping everything in one package worked fine until now, and points at Go's own standard library as evidence that good code needs little folder scaffolding. Structure gets introduced only once the project must produce two binaries, at which point the cmd convention separates each main package from the reusable library code at the root.
“Notice the symmetry: despite being different user interfaces the setup is almost identical. This feels like good validation of our design so far.”
Command line and project structure › Web server application codeRead in context →
If your own code is awkward to construct from outside, importers will find it awkward too — so make a constructor.
Making the CLI's store and reader private was the right call, but wiring it up in main failed to compile with an 'implicit assignment of unexported field' error, and the same failure reappeared once the test moved outside the package. Adding NewCLI, which also wraps the reader in a scanner at construction time, fixed both callers at once and let the reading code be simplified.
“We rightfully made the dependencies of our CLI private (because we don't want them exposed to users of CLIs) but haven't made a way for users to construct it.”
Command line and project structure › Write enough code to make it passRead in context →
Testing only through the public API catches integration problems that same-package tests hide.
The CLI test originally lived in package poker, so it happily touched private fields and package-local helpers — and gave no warning that main could not do the same. Renaming it to poker_test instantly produced undefined-symbol errors for the stub store and the win assertion, revealing exactly what was missing from the package's real surface. The author's rule of thumb: code you can't test easily is code others can't integrate with easily.
“An adage with TDD is that if you cannot test your code then it is probably hard for users of your code to integrate with it.”
Command line and project structure › package mypackage_testRead in context →
3 more claims
Shipping stubs and assertion helpers as public API is a defensible choice, not pollution to avoid at all costs.
The author concedes the trade-off is subjective — test-only code does enlarge the package's surface — but cites the HashiCorp practice described in Mitchell Hashimoto's 'Advanced Testing with Go' plus personal experience on shared packages: exporting a stub player store means importers never have to write their own fake to work with the code.
“An adage with TDD is that if you cannot test your code then it is probably hard for users of your code to integrate with it.”
Command line and project structure › package mypackage_testRead in context →
Small interfaces make the same domain code serve wildly different front ends with almost no extra work.
Because the store is just a PlayerStore and input is just an io.Reader, os.Stdin in production and strings.NewReader in tests drop in interchangeably. After factoring the file-opening into one helper, both mains collapse to the same three steps — load a store from the file, defer its close, hand it to NewPlayerServer or NewCLI — and the author treats that symmetry between an HTTP server and a terminal app as confirmation the design holds up.
“Notice the symmetry: despite being different user interfaces the setup is almost identical. This feels like good validation of our design so far.”
Command line and project structure › Web server application codeRead in context →
Working in thin slices and integrating immediately is what surfaced the design flaw.
The private-field breakage was invisible while the tests lived inside the package; it only appeared the moment the new CLI was wired into a real main. The author draws the general lesson from this episode: aim for fully integrated working software as early as possible, because that is when packaging and visibility mistakes become visible.
“We rightfully made the dependencies of our CLI private (because we don't want them exposed to users of CLIs) but haven't made a way for users to construct it.”
Command line and project structure › Write enough code to make it passRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Private fields block outside construction
Lowercase struct fields are visible only inside their own package, so code in another package cannot fill them in with a struct literal and the compiler rejects the attempt. Keeping dependencies private is still right — you just have to supply an exported constructor that assembles the value for callers.
Mechanism 2
A _test package imitates an importer
Naming the test's package with a _test suffix puts the test outside the package under test, so it can reach only exported names. That turns every accidental reliance on package internals into a compile error, which is exactly the wall an outside user would hit — found while writing tests instead of after shipping.
Mechanism 3
Relative DB path splits the shared file
The database name is a bare relative path, so it resolves against whatever directory the process was started in. Launching the web server and the CLI from their own cmd folders therefore gives each one a private copy of the file, and no error appears — wins simply never show up in the other app. Running both binaries from the same directory keeps them on one database.
Common misreadings
Since a separate _test package is better discipline, internal tests are a bad habit to be eliminated.
The author keeps room for them: when you genuinely need to exercise something internal, write a second test file in the same package as the code. The _test package is the default because it enforces testing the public API, not a prohibition on ever reaching inside.
“An adage with TDD is that if you cannot test your code then it is probably hard for users of your code to integrate with it.”
Command line and project structure › package mypackage_testRead in context →
Pointing both applications at the same filename 'game.db.json' is enough to make them share one database.
That name is relative, so it is resolved against each process's working directory — start the CLI from cmd/cli and the server from cmd/webserver and you get two unrelated files with no warning. Sharing requires launching both from the same place, e.g. building the two binaries and running them from the project root.
“Since our product owner wants the CLI and web server to share the same database, this matters: running both from different directories will silently give each one its own, separate game.db.json.”
Command line and project structure › Some project refactoring firstRead in context →
Key concepts in Build an application
- cmd directory convention
- A top-level cmd folder holding each application's main package, kept apart from the importable library code at the project root.
- Where it applies: Only worth adding once a project builds more than one binary; Go's standard library shows little folder structure is usually needed.
- package mypackage_test
- Naming a test file's package with a _test suffix so the test can only touch exported identifiers, just like a real importer.
- Where it applies: If you genuinely need to test internals, keep a separate same-package test file.
- Exported test helpers
- Shipping stubs and assertion helpers (testing.go) as public API so importers can test against your package without rebuilding fakes.
- Where it applies: Subjective trade-off: it pollutes the package API with test-only code.
- io.Reader
- The read abstraction that lets os.Stdin in main and strings.NewReader in tests be swapped freely.
- Where it applies: Scanner reads only up to a newline per Scan call.
0 more concepts
Reader’s notes on Build an applicationAI reading companion
“cmd is a widely used Go convention for holding the main packages”
Read in context →Worth noting this is convention, not tooling — the Go toolchain gives cmd/ no special meaning. The book is honest about it being a community habit.
“it is good practice not to go over-the-top with package structure”
Read in context →The most useful line in the chapter, and it arrives as an aside. Structure earned by a second binary, not imposed up front.
“running both from different directories will silently give each one its own, separate game.db.json”
Read in context →This caveat reads as a later patch by an editor — the original flow said "go run main.go" happily. Good catch, but it exposes that the shared-database requirement is never actually tested.
7 more notes
“Remember to do the strictly easiest thing first”
Read in context →Hardcoding "Chris" is the move that feels silly and is the whole method. The fake forces the next test to earn the real parser.
“This highlights the importance of integrating your work.”
Read in context →The unexported-field error isn't a mistake in the narrative — it's staged. The author walked us into a wall on purpose so the lesson lands physically.
“if you cannot test your code then it is probably hard for users of your code to integrate with it”
Read in context →The strongest claim here, and it's doing double duty: testability as a proxy for API quality. The _test package suffix turns that belief into a compiler-enforced constraint.
“One could argue that you do not want to pollute your package's API with code to facilitate tests.”
Read in context →He raises the objection and then overrides it with an appeal to HashiCorp plus anecdote. Note what shipping testing.go costs: StubPlayerStore's fields stay private, so importers get a stub they cannot configure.
“We'll also change CLI so it stores a bufio.Scanner instead of the reader”
Read in context →Quiet design shift: the constructor stops being a formality and starts owning a decision. Also why in io.Reader never needed to survive in the struct.
“returns a closing function, so we can close the underlying file”
Read in context →Returning (store, closeFunc, err) is pragmatic but lopsided — on the error paths it hands back a nil close, so the caller must check err before deferring. Easy trap.
“Notice the symmetry: despite being different user interfaces the setup is almost identical.”
Read in context →He calls symmetry validation of the design. Fair — but PlayPoker reads exactly one line and exits, so the CLI isn't really an interface yet. The symmetry is partly because so little has been built.
A reader’s takeAI reading companion
A chapter where the real subject isn't the CLI at all — it's package boundaries, and the CLI is just the pressure that reveals them. The pedagogical spine is excellent: the author deliberately writes code that compiles in tests and fails in main, then uses that failure to argue for package foo_test as a structural safeguard rather than a style preference. The strongest idea is testability-as-API-signal; the most debatable is shipping test helpers in the public package, which he defends by authority and anecdote while leaving a real wart (an exported stub with unexported fields that importers can't populate). Two places where the writing is thinner than the thinking: the product owner's actual requirement — a shared database between two binaries — is only satisfied by a working-directory caveat, never by a test or a design decision, and the closing praise for 'symmetry' flatters a CLI that reads a single line and quits. Still, the arc from 'everything in package main' to two binaries over a shared library, with the integration pain shown rather than described, is about as honest a demonstration of incremental structure as a tutorial can manage.
Read Build an application with these cards beside each passage.
Open in FeedLitraBuild an application: Time
How does injecting Game into CLI change the test seams?
How Build an application unfoldsAI-assisted
This chapter opens where the previous one left off, with a feature that looks small and turns out to expose a design problem: scheduling blind alerts in a poker CLI. The obstacle appears immediately. The natural way to schedule future work in Go is time.AfterFunc, but a test cannot inspect what it was handed, because Go cannot compare function values — there is no way to assert "this callback, at this delay." The author's move is to stop testing the timer and start testing the schedule as data. Scheduling goes behind an injectable BlindAlerter interface; a spy implementation records each ScheduleAlertAt call's duration and blind amount, and the test asserts over that list in table-driven form. Production keeps the real timer, which writes its output for real; the test keeps a recording stand-in. Alongside this, the CLI grows a prompt asking how many players are playing, so the blind schedule can be derived from input rather than hardcoded.
The turn comes from the tests rather than from the feature. To check only that blind alerts get scheduled, the author finds himself wiring up four separate dependencies, and one test asserting at once about input parsing, the schedule, and the recorded winner. He reads that clutter as evidence, not as an inconvenience: a long dependency list is a missing type, and a component needing four collaborators is carrying several jobs. Notably, he says he deliberately tolerated the mess for a while first — the signal he acts on is accumulated concrete friction (assertions piling into one test, setup needed just to construct the thing), not the first extra constructor argument. The extraction gives the alerter and the store a home in a new Game type owning Start and Finish, leaving the CLI to do nothing but interpret user input. Crucially, the refactor is staged: NewCLI's external interface is held fixed while the internals move, so the existing tests stay green and act as the check on the change; only afterwards are the tests themselves rewritten. Changing production code and test code in one move, he argues, is dangerous precisely because it removes the only instrument you have for telling which half broke — hence small steps with the suite re-run after each.
The second half completes the decoupling and then closes out. NewCLI slims down; the CLI is made to depend on a Game interface declaring only Start and Finish, with the concrete implementation renamed TexasHoldem, so the CLI knows nothing about poker rules. A GameSpy recording StartedWith and FinishedWith lets a CLI test simply assert that Start was called with seven players, and bad input is handled explicitly. Worth separating here: those spy assertions prove the CLI wires input through correctly and prove nothing at all about the rules — which is exactly why the blind-schedule and winner-recording checks migrate into TestGame_Start and TestGame_Finish, run against the real implementation. The author describes that migration as copying the old CLI tests but with fewer dependencies, and the shrunken setup is the payoff: it demonstrates retroactively that those assertions had been in the wrong place. The chapter ends on its themes — testing time-dependent code via injected helpers, separation of concerns, methods on non-struct types, and the central discipline: when tests are messy, refactor the tests first, and only suspect the design if they are still messy afterwards.
Key claims, with the lines that support them
Code that schedules work in the future can only be tested if the scheduling is injected rather than called directly.
The author's starting point is that Go cannot compare functions, so there is no way to assert which callback was passed to the timer; he therefore routes scheduling through a dependency and supplies a spy whose ScheduleAlertAt records the duration and the blind amount, letting the tests check the whole schedule as a list of values while production still uses the real timer.
“You cannot compare functions in Go so we'd be unable to test what function has been sent in.”
Time › time.DurationRead in context →
A long list of injected dependencies is a design smell that signals a missing abstraction.
To test only that blind alerts get scheduled, the author has to set up four separate dependencies, and the clutter in that test is his evidence that the component is doing too much; he then extracts a Game that takes the alerter and the store and exposes Start and Finish, leaving the CLI to interpret user input. He notes he deliberately puts up with the mess for a while before refactoring, rather than reacting to the first extra dependency.
“Whenever you have a lot of dependencies for a thing in your system, it implies it's doing too much.”
Time › RefactorRead in context →
Following on from that extraction, the CLI should depend on a Game interface while the concrete implementation is renamed TexasHoldem, so the CLI knows nothing about the actual rules.
The interface is declared with just Start and Finish and every reference to the concrete game inside the CLI is swapped for it; a GameSpy then records StartedWith and FinishedWith, so a CLI test can simply assert that Start was called with 7 players without any poker logic being involved.
“This keeps faithful to the notion that our CLI is oblivious to the actual game we're playing and what happens when you Start and Finish.”
Time › RefactorRead in context →
3 more claims
Once the game is its own type, game-specific assertions belong in the game's tests and the CLI tests should only cover reading input and writing output.
The author moves the blind-schedule and winner-recording checks into TestGame_Start and TestGame_Finish, describing the move as copying the old CLI tests but with fewer dependencies — the reduced setup is the payoff that shows the assertions were in the wrong place.
“Now that we have extracted out Game we should move our game specific assertions into tests separate from CLI.”
Time › RefactorRead in context →
Messy tests are a diagnostic signal about the design, not just an aesthetic problem.
He points to what was visibly wrong in his own suite — one test checking input, the schedule and the winner all at once, plus too many dependencies to construct — and argues that the right response is to refactor the tests; if they are still a mess afterwards, the design behind them is probably flawed.
“If you've done this and they're still a mess it is very likely pointing to a flaw in your design”
Time › More examples of good separation of concernsRead in context →
Refactor in small steps that keep the suite passing, and never rework production code and test code in the same move.
He calls it dangerous to change both at once and recommends re-running the tests after each small change, so you are never stranded with a broken build; the concrete discipline is leaving NewCLI's external interface untouched during the extraction precisely so client code and tests do not have to change yet.
“It would've been dangerous to refactor both the test code and the production code at the same time, so we first refactored the production code (in the current state we couldn't improve the tests much) without changing its interface so we could rely on our tests as much as we could while changing things.”
Time › More examples of good separation of concernsRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Why timing needs an interface
Go cannot compare function values, so a test has no way to check which callback was handed to time.AfterFunc. Putting the scheduling behind an injectable interface lets a recording stand-in capture each duration and amount instead, so timing behaviour becomes ordinary data a test can assert on while real output still goes through the live timer.
Mechanism 2
Too many dependencies means a missing type
When one component needs four collaborators wired up just to test alert scheduling, that count is evidence it is carrying several jobs at once. Moving the alerter and the store into a Game that owns Start and Finish gives each responsibility its own home, which is why the long list shrinks and the tests get smaller after the extraction.
Mechanism 3
Change one side at a time
Tests are the only check on a refactor, so rewriting production code and test code together removes that check and leaves you unable to tell which change broke things. Holding NewCLI's external interface fixed while the internals are restructured keeps the existing tests green, and only then are the tests themselves rewritten, with the suite re-run after every small step.
Common misreadings
If the spy confirms Start and Finish were called with the right values, the game logic itself has been tested.
A spy only records how it was called, so those assertions prove the CLI wires input through correctly and say nothing about whether the rules are right; that is why the blind-schedule and winner-recording checks are moved into the game's own tests against the real implementation.
“Now that we have extracted out Game we should move our game specific assertions into tests separate from CLI.”
Time › RefactorRead in context →
As soon as tests look ugly, the design is wrong and should be redesigned.
The author's rule has a step before that: first refactor the tests themselves, and treat the design as the culprit only if they are still a mess after that cleanup. The signals he actually acts on are concrete — assertions piling up in one test and dependencies needed just to build it.
“If you've done this and they're still a mess it is very likely pointing to a flaw in your design”
Time › More examples of good separation of concernsRead in context →
Key concepts in Build an application
- Dependency Injection
- Pass collaborators in from outside so tests can substitute fakes. Passing collaborators in rather than reaching for globals, so the dependency list documents the design.
- Where it applies: Helps until the injected list grows so long it signals a design problem. It documents intent only if the injected list genuinely matches responsibilities.
- Spy
- A test double that records the calls made to it so you can assert on them. A stand-in object that records how it was called so a test can assert on those calls.
- Where it applies: Only records; it does not schedule real time-based work. Useful for verifying interactions like Start/Finish; it says nothing about whether real game logic is correct.
- Test Double
- Stand-in objects for real collaborators; a dummy just fills a parameter slot.
- Where it applies: A dummy must never be asserted on, since it is never really used.
- Table driven tests
- One test body run over a list of input/expected pairs.
- Where it applies: Best when every case exercises identical logic with differing values.
- Interface func adapter
- A named func type with the interface method on it, so a plain function satisfies a one-method interface. Defining a new type around a function or map so you can hang methods on it and satisfy interfaces.
- Where it applies: Idiomatic mainly for interfaces with a single method. Applies to any Go type, not just structs; most handy for one-method interfaces.
- Interface
- A named set of methods a caller depends on instead of a concrete type.
- Where it applies: Here it only covers Start and Finish, so CLI stays ignorant of the real game's workings.
1 more concept
- time.After
- Standard library timing helpers that fire after, or repeatedly over, a duration.
- Where it applies: This chapter only points at the docs; it does not cover cancellation of scheduled work.
Reader’s notes on Build an applicationAI reading companion
“You cannot compare functions in Go so we'd be unable to test what function has been sent in.”
Read in context →A language limitation becomes the design driver. The wrapper interface isn't chosen for elegance — it's chosen because the test can't otherwise see anything. Honest about it, at least.
“By labelling it as a "dummy" it is clear to the reader of the test that it is not important.”
Read in context →Naming as documentation for absence. A tiny move, but it's the difference between a reader asking "why is this here?" and gliding past.
“60 * time.Minute, 800”
Read in context →Note the table quietly contradicts the spec above — the requirements said 100, 200, 400, 600, 1000, 2000. The code says 100…600, 800, 1000. Nobody flags it. Tests are now the real specification.
10 more notes
“Remember, we are free to commit whatever sins we need to make this work.”
Read in context →The permission to write bad code is itself the discipline — it only works because the refactor step is non-negotiable. Take the license without the follow-through and you just have sins.
“Whenever you have a lot of dependencies for a thing in your system, it implies it's doing too much. Visually we can see it in how cluttered our test is.”
Read in context →This is the chapter's real thesis: the test is a pressure gauge for the design. The ugliness isn't a testing problem to be worked around — it's a message.
“we don't want to change the test code and the client code at the same time as that is too much to juggle”
Read in context →The quietest wisdom here. Refactoring safety comes from holding one side fixed — you need an unmoved reference point or you can't tell what broke.
“Remember that any type can implement an interface, not just structs.”
Read in context →The BlindAlerterFunc trick is Go's http.HandlerFunc idiom in miniature. Worth noticing that it exists to spare callers the ceremony of an empty struct — pure ergonomics.
“This keeps faithful to the notion that our CLI is oblivious to the actual game we're playing”
Read in context →The rename is the whole design argument compressed into a word choice. "Game" as interface, "TexasHoldem" as detail — the name tells you which side of the boundary each lives on.
“wantPrompt := poker.PlayerPrompt + "you're so silly”
Read in context →Deliberately placeholder wording so the test pins the behaviour, not the copy. Honest about the fact that at this moment he doesn't yet know what the message should say.
“can you refactor/remove some of them and still be confident it all works”
Read in context →Rare to see a TDD text tell you to delete tests. Tests are inventory too — the cost of keeping them is paid on every future change.
“We could visually see it was cluttered; it is so important to listen to your tests.”
Read in context →The strongest claim in the chapter: messy tests are a design smell, not a testing problem. Everything earlier was setup for this sentence.
“so we first refactored the production code (in the current state we couldn't improve the tests much) without changing its interface”
Read in context →The ordering is the real craft lesson — never let both sides of the safety net move at once. Most people skip this and then can't tell which half broke.
“in Go you can add methods to types, not just structs”
Read in context →The BlindAlerterFunc trick and the Blog map are the same idea twice: an interface isn't a demand for a class, just for behaviour. Lovely that Blog as a bare map becomes an HTTP server.
A reader’s takeAI reading companion
This chapter looks like a feature chapter — schedule blind alerts in a poker CLI — but it is really a chapter about letting tests tell you where your design went wrong, and it earns that thesis by walking into the mess first rather than architecting around it. The opening move is the most honest in the book: the wrapper interface over a scheduling function is not chosen for elegance but because Go cannot compare functions, so without the wrapper the test can see nothing at all. A language limitation becomes the design driver, and the author says so plainly. From there the craft is in small things — a spy named 'dummy' so the reader stops asking why it exists; placeholder prompt wording ('you're so silly') that pins behaviour rather than copy and admits he doesn't yet know what the message should say; time.
AfterFunc doing the real work behind the seam. Worth noticing that the alert table quietly contradicts the stated requirements (800 where the spec said nothing of the kind) and nobody flags it: by this point the tests, not the prose, are the specification. That is TDD's reality and its hazard in one unremarked line. The hinge comes when NewCLI accumulates four dependencies and the test becomes visibly cluttered. The chapter's answer is not to tidy the test but to read it: a lot of dependencies means the thing is doing too much, and the ugliness is a message, not an inconvenience. Out comes Game, then TexasHoldem behind it — a rename that compresses the entire boundary argument into word choice, interface for the role, concrete name for the detail — plus a GameSpy to verify Start/Finish and proper handling of bad input.
Continue reading
The second half's sharpest teaching is procedural rather than architectural: refactor the production code first without changing its interface, then the tests, because you need one unmoved reference point or you cannot tell which half broke. Most people move both and then debug blind. Two smaller gifts: 'we are free to commit whatever sins we need to make this work' only functions as discipline because the refactor step is non-negotiable — take the licence without the follow-through and you simply have sins; and the BlindAlerterFunc trick, Go's http.HandlerFunc idiom in miniature, reminds you that any type can satisfy an interface, which is why a bare map can become a blog server.
Pure ergonomics, sparing callers an empty struct. Rarest of all, the chapter tells you to delete tests — can you remove some and still be confident? — treating tests as inventory whose carrying cost is paid on every future change. What I'd tell a reader: don't skim the cluttered-test moment to reach the clean Game interface. The interface is the answer; the clutter is the method. Learning to feel a test's awkwardness as information about the production code is the transferable skill here, and it survives long after you've forgotten what a blind alerter is.
Read Build an application with these cards beside each passage.
Open in FeedLitraBuild an application: Revisiting time, with testing/synctest
Why doesn't holding a sync.Mutex count as durably blocked?
How Build an application unfoldsAI-assisted
Opening: naming the gap left by the old solution. The chapter begins by looking back at the earlier "time" chapter, where the poker CLI scheduled blind-increase alerts with time.AfterFunc. Because functions can't be compared and the callback runs on its own goroutine after real time passes, the original fix was dependency injection: a BlindAlerter interface plus a SpyBlindAlerter that merely records the duration and amount it was asked to schedule. The author grants that this design is sound and the tests are fast, then puts his finger on what it leaves untested — the one piece of code that genuinely calls time.AfterFunc and prints something never runs in a test at all. That omission is framed as a deliberate trade-off (wait real minutes, or shrink durations and hope the machine keeps up), which sets up the arrival of Go 1.25's testing/synctest as a third option. A short primer follows: a bubble with a fake clock starting at midnight 2000-01-01, time advancing only when every goroutine is durably blocked (sleeps, in-bubble channel receives, Cond.Wait, WaitGroup.Wait — but not mutex locks), synctest.Test waiting for all spawned goroutines, and synctest.Wait letting background work settle.
Development: earning the tool by feeling the pain. Rather than jumping straight to synctest, the chapter runs the ordinary TDD loop first. A real-time test schedules a 5-second alert, sleeps 6 seconds, and asserts on a bytes.Buffer; it fails to compile, then passes once BlindAlerter, BlindAlerterFunc, and StdOutAlerter exist. It also takes six real seconds — multiplied across the game's eleven alerts, some over an hour apart, that's untenable, which is exactly the problem synctest addresses. The first synctest attempt is deliberately naive: drop the sleep and expect the fake clock to do the work. It fails, teaching the key point that a bubble's clock doesn't run forward on its own; it only moves when something inside blocks waiting on it. Restoring the sleep makes the test pass instantly — apparently done.
Turn: two race-detector failures that reframe the lesson. The chapter's pivot is a habitual -race run, which reports a data race between the buffer read and the callback's write. The stack trace shows the writing goroutine already finished, so the assertion could never actually fail — but the race detector asks whether anything guarantees ordering, and sleeping, however generously, guarantees nothing. The author notes this race was lurking in the original real-time version too, unnoticed because nobody runs -race repeatedly on a six-second test. Adding synctest.Wait() as a genuine synchronization point produces repeatably clean runs. Then a second test — asserting that nothing has happened yet — triggers a second, subtler race: with no other goroutine to bound it and no upcoming deadline, Wait() lets the runtime fire the pending 5-second timer to make progress, so the callback runs concurrently with the "nothing yet" check on an unguarded buffer.
Close: fix the design, not the symptom. A lock or atomic.Pointer would silence the race, but the chapter instead applies the book's recurring principle — test pain is a design signal. StdOutAlerter was doing two jobs, deciding the message and performing the side effect, and nothing about scheduling a blind alert needs to know about io.Writer. The refactor replaces it with NewAlerter, returning the alerter plus a <-chan string; the interface types stay untouched, and printing becomes main's concern. Invoking the proverb about sharing memory by communicating, the resulting test drops Wait(), the sleep, and any guard type: a non-blocking select with default confirms nothing has arrived at virtual time zero, and a plain blocking receive lets the bubble jump time to the timer's firing. It stays green under repeated -race because no shared memory remains. The chapter positions synctest as complementary to the old spy rather than a replacement — the spy checks scheduling arithmetic, synctest checks that the mechanism works — then recaps the lessons and lists gotchas (deadlock on still-blocked goroutines, bubble-bound channels and timers, non-durably-blocking real I/O, and AfterFunc callbacks needing their own locks for shared state). It ends with links and a candid note on the chapter being the book's first written with AI assistance, stressing that claims were verified by running real tests and that the design changed mid-draft for the better.
Key claims, with the lines that support them
Testing the alerter only through a spy leaves the code that really schedules and emits alerts untested.
The author points out that the spy-based tests assert only bookkeeping, such as 'ScheduleAlertAt was called with 10 minutes and 200', so the part that actually calls time.AfterFunc and produces output — the place a bug would live — is never run. He frames this as a deliberate trade-off rather than carelessness, since a realistic real-time test would take minutes or force unrealistically tiny durations.
“The one piece of code that actually calls time.AfterFunc and prints something, the part with the real bug potential, never gets exercised by a test.”
Revisiting time, with testing/synctestRead in context →
A synctest bubble does not fast-forward on its own, so the test must still say what it is waiting for.
The author deliberately writes an over-optimistic version that drops the sleep entirely once the test runs in a bubble; the buffer comes back empty and the test fails instantly. Putting the six-second sleep back makes it pass in effectively zero real time, showing the clock jumps only because something asked it to.
“A bubble's fake clock doesn't run itself forward on a timer of its own. It only advances when something in the bubble durably blocks, waiting for it to.”
Revisiting time, with testing/synctest › Introducing synctestRead in context →
Having slept long enough is not the same as having synchronized, so the passing test still contained a real data race.
Running the green bubble test under -race produces a warning, and the report even labels the writing goroutine as finished — the write really did precede the read on every run. The author uses that to separate two questions: whether it went wrong this time, versus whether anything guarantees it cannot. He adds that the original plain real-time version had the same race all along; nobody thought to run -race repeatedly on a six-second test.
“But the race detector isn't asking "did this go wrong this time?"; it's asking "is there anything that guarantees it can't?" time.”
Revisiting time, with testing/synctest › Introducing synctestRead in context →
3 more claims
synctest.Wait() is not a blanket safety net: with nothing else in the bubble to bound it, it can let the clock run far enough to fire the very timer you were asserting had not fired.
Following on from the previous point, the author adds Wait() to fix the race and it works for the 'alert has arrived' check — but when he writes a second check asserting nothing has printed yet, -race flags that first line reliably. The reasoning: with no other goroutine and no upcoming deadline, firing the pending five-second timer is the runtime's only way to make progress, so the callback goroutine runs alongside the assertion.
“The issue is that Wait() alone has nothing bounding how far it's willing to let the fake clock run.”
Revisiting time, with testing/synctest › Write the test firstRead in context →
The painful test was pointing at a design flaw, so the fix was to redesign the alerter rather than bolt on a mutex.
A lock or an atomic pointer would have silenced the race, but the author notes that deciding the message and performing the printing are two jobs, and nothing about scheduling a poker blind requires knowing about an io.Writer. Reworking the alerter to send the finished message down a channel removes the shared buffer entirely; the resulting test needs no Wait(), no guard type and no sleep, and stays green under repeated -race runs.
“That's the shape of tension this book keeps coming back to: if your tests are causing you pain, listen to that signal and think about the design of your code.”
Revisiting time, with testing/synctest › Write the test firstRead in context →
The spy and synctest answer different questions, so the new approach supplements the old one instead of replacing it.
The author keeps the spy as the right tool for checking arithmetic — whether the correct amounts are scheduled at the correct offsets for a given player count — where real timing is irrelevant. synctest instead closes the gap such a spy always leaves: whether the scheduling mechanism itself works.
“The one piece of code that actually calls time.AfterFunc and prints something, the part with the real bug potential, never gets exercised by a test.”
Revisiting time, with testing/synctestRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Fake time only moves when everyone is stuck
Inside a synctest bubble the clock is virtual and never ticks by itself; it jumps forward only when every goroutine in the bubble is blocked in a way that only another bubble goroutine could undo, such as a sleep or a channel receive. So the test still has to express what it is waiting for, and the reward is that the wait costs no real seconds.
Mechanism 2
Why a generous sleep is still a race
A sleep on one goroutine makes no promise about what another goroutine has finished doing to shared memory; it only makes the lucky ordering overwhelmingly likely. The race detector checks whether an ordering is guaranteed, not whether it happened, so lengthening the sleep merely hides the problem outside -race.
Mechanism 3
An unbounded Wait lets the timer fire
synctest.Wait() returns once every other goroutine is durably blocked, but nothing caps how far the clock may run while it decides. With no other goroutine and no pending deadline, the only way to make progress is to fire the queued timer, which spawns the callback goroutine concurrently with the assertion that was supposed to see nothing yet.
Common misreadings
Once the test runs inside a synctest bubble, the fake clock will skip ahead by itself, so you can delete the sleeps and just assert.
The clock advances only when every goroutine in the bubble is durably blocked and something is waiting on time; the test must still express the wait, it simply no longer costs real seconds. The author demonstrates this with a sleepless version that fails on an empty buffer and passes again once the sleep is restored.
“A bubble's fake clock doesn't run itself forward on a timer of its own. It only advances when something in the bubble durably blocks, waiting for it to.”
Revisiting time, with testing/synctest › Introducing synctestRead in context →
If the write always finishes before the read in practice, the race detector warning is a technicality you can ignore — or at worst something a longer sleep will cure.
The warning is about the absence of a guaranteed ordering, not about what happened on this run, so a longer sleep only reduces how often it shows up outside -race. Real synchronization — a Wait() at the right moment, or better, handing the value across a channel — is what actually removes it.
“But the race detector isn't asking "did this go wrong this time?"; it's asking "is there anything that guarantees it can't?" time.”
Revisiting time, with testing/synctest › Introducing synctestRead in context →
Key concepts in Build an application
- testing/synctest bubble
- An isolated run of a function where the time package uses a fake clock starting at midnight UTC, 1 Jan 2000.
- Where it applies: Real network and file I/O are not durably blocking, so they cannot be driven by the fake clock.
- Durably blocked
- Blocked in a way only another goroutine in the same bubble can undo; fake time advances only then.
- Where it applies: Locking a sync.Mutex does not count, because mutexes are typically held only briefly.
- Data race
- Two goroutines touching shared memory with nothing enforcing an order, even if one practically always wins.
- Where it applies: A longer sleep never fixes it; it only hides it outside -race.
- Channel
- Hands the finished message across a goroutine boundary instead of writing into shared state.
- Where it applies: Channels are tied to the bubble they were created in; using one from outside its bubble panics.
0 more concepts
Reader’s notes on Build an applicationAI reading companion
“They never let a real alerter run.”
Read in context →The honest confession that makes the chapter worth reading. Most books would leave the DI victory lap unexamined; naming the untested hole is the actual lesson.
“That's not an oversight, it's a trade-off.”
Read in context →Good discipline: the old chapter wasn't wrong, it was constrained. Tooling changed the constraint, not the reasoning.
“surely the fake clock handles that for us now? It doesn't”
Read in context →Deliberately walking into the wrong intuition first is the right teaching move — the mental model "fake time is fast-forward" has to break before "fake time is demand-driven" can land.
9 more notes
“out of habit: go test -race”
Read in context →The casual framing is doing real work. The chapter's sharpest point arrives as an afterthought, which is exactly how such bugs arrive in real life.
“it's asking "is there anything that guarantees it can't?”
Read in context →The cleanest one-line definition of the race detector I've read. Sleeping is a probability argument; synchronization is a proof.
“who runs -race, repeatedly, on a test that already takes six real seconds”
Read in context →Lovely inversion: the slow test didn't just waste time, it concealed a defect by discouraging the one tool that would find it. Slowness as an epistemic cost, not merely an inconvenience.
“Wait() alone has nothing bounding how far it's willing to let the fake clock run”
Read in context →This is the genuinely surprising bit — Wait() can advance time to make progress, so asserting "nothing yet" is the most dangerous assertion in a bubble. I'd want this louder than a mid-paragraph sentence.
“if your tests are causing you pain, listen to that signal”
Read in context →The pivot the whole chapter was built to earn. Note what's refused: the mutex fix would have worked and taught nothing.
“which, tellingly, no longer had anything to do with stdout”
Read in context →The name was lying the whole time and nobody noticed until the race forced a look. A misleading name as a leading indicator of a misplaced responsibility.
“Notice what's gone: no synctest.Wait() anywhere”
Read in context →The payoff is subtractive — the new design needs less of the new tool. A chapter introducing synctest ending by using barely any of it is a confident move.
“synctest didn't replace that need; it fills the coverage gap”
Read in context →Resists the upgrade narrative. Spy answers "did I ask for the right thing", synctest answers "does asking work" — different questions, both needed.
“This is the first chapter in the book written with AI assistance”
Read in context →The disclosure earns credibility precisely because it describes verification rather than generation — the gotchas exist because real tests failed. Though "if something reads oddly, that's on me" is a slightly too-tidy absorption of responsibility.
A reader’s takeAI reading companion
A chapter whose real subject isn't testing/synctest but the gap between a test that passes and a test that proves something. Its structure is quietly excellent: each version of the test looks finished, passes, and is then undone by a single habitual -race run — twice, including on the assertion the author explicitly says they were confident about. The most valuable insight isn't the API but the epistemology: sleeping long enough is a probability argument, not a synchronization guarantee, and a slow test is dangerous not because it wastes seconds but because its slowness deters the very tool that would expose its bug.
The second race — where synctest.Wait(), with nothing else in the bubble to bound it, lets the fake clock run forward and fires the timer concurrently with the assertion — is genuinely counterintuitive and under-signposted for how easily it would bite a reader; it deserves more than a mid-paragraph explanation. The refactor to a channel is the book's house style at its best: the mutex fix would have worked and taught nothing, and the fact that StdOutAlerter had stopped being about stdout was a name quietly flagging a misplaced responsibility all along. The ending resists two temptations worth noting — it does not declare the old SpyBlindAlerter obsolete, and it closes with a design that barely uses the tool the chapter was ostensibly selling.
Continue reading
The AI-assistance note is well-placed and lands because it describes verification over generation, though its final absorption of blame is a touch too neat.
Read Build an application with these cards beside each passage.
Open in FeedLitraBuild an application: WebSockets
How does this chapter finally remove the arbitrary time.Sleep from the WebSocket test?
How Build an application unfoldsAI-assisted
Opening — the same game, a new doorway. The chapter begins by framing the move to the browser as a change of scenery rather than a change of substance: the poker application's requirements are declared identical to the terminal version, and only the channel through which input arrives is different. That framing does real work, because it licenses reusing the Game abstraction built in the previous chapter untouched. So the first steps are plumbing. HTML is served through html/template, and here the author pauses to make an explicit cost argument about testing markup: driving a real browser is disproportionate machinery, asserting on the rendered HTML character for character makes the test brittle against any cosmetic edit, and verifying merely that the template was asked to render proves almost nothing about the page. With a single template in the project, the cheapest bet is to skip the test — a decision presented as a judgement about payback, not a general license to leave code untested. Then comes the WebSocket on /ws, driven by a test that deliberately mirrors the terminal one: send the player count, send the winner's name, assert the game was started and finished with those values, reusing the same stand-in game as before.
Development — ugly code, held in place by a test. The first working handler is openly bad. It swallows every error, and the test that pins it leans on a hard-coded ten-millisecond pause, which the author labels bad practice at the moment of writing it rather than in hindsight. The point being made is about sequence: getting to green first is what makes the cleanup safe, because the test is the thing that will catch a mistake during refactoring. So the mess is kept only long enough to reach a passing state, then dismantled — a shared upgrader is extracted, error handling moves into common helper functions. The arbitrary pause is flagged as still unresolved and deferred, which sets up the chapter's final movement.
Turn — the destination can't be known in advance. The structural problem arrives when blind alerts need to reach the browser. Each incoming request produces its own fresh response writer, which is then upgraded into its own socket; there is simply no single output destination in existence at the moment the server's dependencies are assembled. The resolution is to move the destination out of construction and into the call: BlindAlerter and Game are refactored to accept an io.Writer, so whichever connection is live when the alert fires is the one that receives it. Game is then wired into PlayerServer, and the socket is wrapped in a playerServerWS. The bridge turns out to be tiny — the game logic needs nothing more than somewhere to send bytes, so the compiler's complaint about a missing Write method is the whole obstacle, and supplying that method is the whole change. Shortening the blind-raise interval and opening the page shows the amounts climbing live. The author is careful to label this for what it is: a manual eyeball check, not a test.
Close — asserting the message, not the call. Having only eyeballed it, the author argues for testing what the server actually sends over the connection rather than spying on the function call that produced it, on the grounds that tests pinned to internal calls break as soon as the implementation is rearranged even when visible behaviour is unchanged. (This is the same reasoning that earlier rejected "check that the template was asked to render": a recorded call confirms only that one piece of code talked to another in a particular way.) The stand-in game is still used, but now given a canned alert that it writes to whatever destination it is handed, so the test waits for that exact text to arrive over the socket. That immediately exposes the async hazard: reading from a socket blocks forever waiting for a message that may never come, freezing the run instead of failing it. The fix splits by case. For something that must arrive, the assertion runs in its own goroutine and is raced against a timer channel, so absence becomes a reported failure. For state that merely arrives late, a retry helper re-checks until a deadline. The payoff closes the loop opened mid-chapter: the hard-coded pause can now be deleted from the shared assertion helpers. Both of the chapter's acknowledged shortcuts — the swallowed errors and the guessed sleep — are gone by the end, which is the actual argument being demonstrated.
Key claims, with the lines that support them
The game abstraction built for the terminal can be carried over to the browser untouched, because only the way input arrives has changed.
The author notes the requirements are identical and only the input channel differs, then drives the browser version with a test that mirrors the terminal one: it sends the number of players and then the winner's name over the socket and checks that the game was started and finished with those values, reusing the same stand-in game from the previous chapter.
“This is the same requirements we have now, just a different way of getting the inputs; so we should look to re-use this concept if we can.”
WebSockets › RefactorRead in context →
Some markup simply isn't worth a test, because every way of testing it costs more than it pays back.
The author weighs three options and rejects each: driving a real browser is far more machinery than the problem deserves, matching the rendered HTML character for character makes the test break on any cosmetic edit, and merely checking that the template was asked to render proves almost nothing about the page. With a single template in the project, skipping the test is the cheaper bet.
“So in the book "Learn Go with Tests" for the first time, we're not going to write a test.”
WebSockets › How do we test we return the correct markup?Read in context →
It is fine to ship deliberately ugly code as long as a test is holding it in place, since the test is what makes the cleanup safe.
The first working version swallows every error and the test leans on a hard-coded ten-millisecond pause; the author calls both out as bad, keeps them only to get to green, then extracts a shared upgrader and helper functions behind the protection of that test. The arbitrary pause is openly admitted as unresolved for the moment, to be dealt with later.
“We have nasty, horrible, working software backed by a test, so now we are free to make it nice and know we won't break anything accidentally.”
WebSockets › RefactorRead in context →
3 more claims
Giving the socket wrapper an output interface is all it takes to push live blind alerts to the browser.
Following from the per-request destination problem, the author points out that the compiler refuses the wrapper as an output target until a Write method exists; adding it is the entire change. Shortening the blind-raise interval and opening the page shows the amounts climbing in real time — though the author is clear this is a manual eyeball check, not an automated test.
“We've managed to re-use Game with PlayerServer and it has taken care of all the details.”
WebSockets › Write enough code to make it passRead in context →
Better to assert the real message coming back over the connection than to spy on the function call that produced it.
Having only eyeballed the browser, the author argues for testing what the server actually sends, because tests that watch internal calls break as soon as the implementation is reshuffled. The stand-in game is still used, but now given a canned alert that it writes to the supplied destination, so the test waits for that exact text to arrive over the socket.
“Equally we should be able to test the messages our server sends back over the websocket connection.”
WebSockets › Let's not write a test!Read in context →
Asynchronous tests should neither hang nor sleep on a guess; use a timeout for things that must arrive and a retry loop for things that arrive eventually.
Reading from the socket blocks indefinitely for a message that is never sent, so the test freezes until a deadline helper races the read against a timer and reports failure instead. For assertions that are merely slow to become true, a retry helper re-checks the recorded state until its deadline, which lets the author delete the hard-coded pause from the shared assertion helpers.
“We should never have tests that hang so let's introduce a way of handling code that we want to timeout.”
WebSockets › Write the minimal amount of code for the test to run and check the failing test outputRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Each request brings its own output target
A browser request produces a brand-new response writer that gets upgraded into its own socket, so there is no single destination that exists when the server's dependencies are built. The author therefore moves the alert destination out of the alerter's construction and into the call itself, so whichever connection is live at that moment receives the alerts.
Mechanism 2
A tiny Write method bridges socket and game
The game logic only needs somewhere to send bytes, so wrapping the socket in a small type that carries a Write method makes the connection usable as an ordinary output destination. Once the compiler stops complaining about the missing method, the existing game code streams blind alerts to the browser with no changes of its own.
Mechanism 3
Why blocking reads need deadlines
Reading from a socket waits forever for a message that may never arrive, which freezes the whole test run instead of failing it. The author runs the assertion in a separate goroutine and races it against a timer channel, so a missing message turns into a failure; for state that merely arrives late, a loop re-checks until a deadline instead of pausing for a guessed duration.
Common misreadings
Since a stand-in collaborator records exactly which calls were made, checking those recorded calls is the strongest proof the feature works.
Recorded calls only confirm that one piece of code talked to another in a particular way, which is why the author rejects checking that the template was asked to render and instead asserts that the real alert text comes back over the connection. Tests pinned to internal calls fail the moment the implementation is rearranged, even when the visible behaviour is unchanged.
“Equally we should be able to test the messages our server sends back over the websocket connection.”
WebSockets › Let's not write a test!Read in context →
Because the author shipped a handler that ignores every error and a test that just pauses for ten milliseconds, that style is acceptable to leave in place.
Both were temporary scaffolding: the author labels the pause bad practice at the moment of writing it, and the only reason the mess is tolerable is that a passing test makes the subsequent cleanup safe. By the end the shortcuts are gone — the error handling is pulled into shared code and the pause is replaced by a deadline and a retry loop.
“We have nasty, horrible, working software backed by a test, so now we are free to make it nice and know we won't break anything accidentally.”
WebSockets › RefactorRead in context →
Key concepts in Build an application
- WebSockets
- A protocol keeping one TCP connection open so server and browser can message each other at any time.
- Where it applies: Built into modern browsers only; older browsers are left unsupported here.
- html/template
- Standard-library package that parses an HTML file and writes the rendered result to an io.Writer.
- Where it applies: Parsing per request hits the filesystem needlessly, so parse once at construction.
- io.Writer
- The generic output destination that lets the same alerter write to stdout or to a WebSocket connection. A tiny interface with just Write, so anything that can accept bytes can stand in as output.
- Where it applies: Only solves where output goes, not when or how it is scheduled. Only covers writing bytes; it says nothing about framing, errors upstream, or reading.
- Iterative application development
- Doing the smallest useful piece of work first, then growing it, instead of building everything at once.
- Where it applies: Tolerates temporarily ugly code only because tests guard the refactor.
- Spy
- A stand-in collaborator that records the calls made to it so a test can assert on them. A stand-in that records how it was called, and here also sends a canned message out.
- Where it applies: Confirms interactions only, not real blind scheduling behaviour. Useful for implementation details, but brittle when you refactor what it observes.
- time.After
- A channel that fires after a duration, used in select to race a timeout against an assertion.
- Where it applies: Gives a deadline, not cancellation; the goroutine running assert keeps going.
1 more concept
- Dependency Injection
- Hand the server its Game from outside instead of letting it build one.
- Where it applies: Helps reuse and testing; does not by itself fix async timing in tests.
Reader’s notes on Build an applicationAI reading companion
“So in the book "Learn Go with Tests" for the first time, we're not going to write a test.”
Read in context →The most honest line in the chapter. A TDD book earning credibility by naming where TDD stops paying. The preceding three-option cost analysis is what makes it a judgment rather than laziness.
“Let's go with that for now but acknowledge that putting in arbitrary sleeps into tests is very bad practice.”
Read in context →Confessed and left in. The sleep is a race, not a delay — 10ms works until CI is loaded. Honest, but the debt never gets repaid in this segment.
“We have nasty, horrible, working software backed by a test, so now we are free to make it nice”
Read in context →The whole argument for red-green-refactor in one sentence: the test is what buys permission to be careless first.
11 more notes
“For every request we get a new http.ResponseWriter which we then upgrade to *websocket.Conn. So we can't know when constructing our dependencies where our alerts need to go.”
Read in context →Here's the real design pressure of the chapter. Stdout is a process-lifetime singleton; a socket is per-request. The fix — move the destination from construction time to call time — is the actual lesson, and it's bigger than WebSockets.
“The requirements of CLI and Server are the same! It's just the delivery mechanism is different.”
Read in context →The payoff of last chapter's Game abstraction, collected in public. Though note it wasn't quite free — the interface had to change shape to survive the second caller.
“(Yes, we're ignoring a lot of errors right now!)”
Read in context →Three discarded errors on three lines. The parenthetical does a lot of work — it buys the reader's patience without pretending the code is fine.
“p.game.Start(numberOfPlayers, io.Discard) //todo: Don't discard the blinds messages!”
Read in context →A comment as a bookmark in the narrative. Nice teaching move: the seam is already there, only the wiring is missing — the next step will be small.
“I chose to create a symlink (ln -s ../../game.html game.html)”
Read in context →A relative path to a template file is a latent bug dressed as a working directory problem — the server only runs from one folder. The symlink hides it rather than solving it; embedding would.
“Let's not write a test!”
Read in context →The most honest sentence in the book. TDD's dogma relaxed exactly where it should be: exploration isn't verification. The caveat — commit first — is what makes it safe rather than sloppy.
“It seems the obvious thing to do, would be to make it so playerServerWS does implement io.Writer”
Read in context →This is the whole Go design argument in one move: the interface was never declared, it was discovered. Three lines and the websocket becomes a pipe the game already knew how to talk to.
“This seems too easy! Try and run the application and see if it works.”
Read in context →The author lets his own disbelief stand on the page instead of smoothing it over. Suspicion of easiness is a real engineering instinct, and he resolves it by running the thing, not by arguing.
“it's often spy tests that start failing because they are usually checking implementation details that you're trying to change”
Read in context →The sharpest claim here: a test that breaks on refactor isn't protecting you, it's taxing you. Though the GameSpy that follows is still a spy — he's narrowed what it asserts, not escaped mocking.
“You should find the test hangs forever. This is because ws.ReadMessage() will block until it gets a message”
Read in context →A hang is worse than a failure — it tells you nothing and blocks everything. Good that the fix is a general tool, within, rather than a patch at the call site.
“Through the journey we have made mistakes but with the TDD flow we have never been very far away from working software.”
Read in context →The real promise of the practice, stated modestly: not fewer mistakes, just shorter distance back. That's a claim about recovery time, not correctness.
A reader’s takeAI reading companion
This chapter is the victory lap of the poker trilogy, and it earns the lap by being honest about what the lap costs. The arc is simple to state: take the CLI game that already works, serve an HTML page, open a WebSocket, let the browser declare the winner, and push blind alerts back down the wire. The arc is also the point — almost nothing about the game changes. What changes is delivery. The line 'The requirements of CLI and Server are the same! It's just the delivery mechanism is different' is the thesis of three chapters collected in one sentence, and the chapter's real work is showing that such payoffs are never quite free: the Game interface still had to change shape to survive its second caller.
That reshaping is the chapter's genuine lesson, and it is bigger than WebSockets. Stdout is a process-lifetime singleton; a socket is per-request, born from an http.ResponseWriter that doesn't exist when you wire your dependencies. So the destination cannot live at construction time — it has to move to call time. BlindAlerter and Game both grow an io.Writer parameter, and that is the whole fix. Then comes the move that makes it feel inevitable rather than clever: wrap the connection in playerServerWS and give it a Write method, three lines, and the socket becomes something the game already knew how to talk to. The interface was never declared for this purpose; it was discovered to already fit. That is the Go design argument in miniature, and the chapter demonstrates it rather than lecturing it.
Continue reading
What I most respect is where the author steps off his own doctrine. 'Let's not write a test!' — twice, in a book whose title is Learn Go with Tests — is the most credible writing in it, because it arrives after an explicit three-way cost analysis and with a safety rail attached: commit first, then explore. That is judgement, not laziness. The same honesty runs through the parentheticals: 'Yes, we're ignoring a lot of errors right now!', the io.Discard with a //todo beside it, the confessed arbitrary sleeps. These buy the reader's patience without pretending the code is fine. But note which debts get repaid and which don't. The io.Discard bookmark is cashed in within a page. The sleeps are eventually replaced by within and retryUntil — a real repayment, and a general tool rather than a patch at the call site, prompted by the best teaching moment in the chapter: a test that hangs on ws.ReadMessage, because a hang tells you nothing and blocks everything, which is strictly worse than a failure. The debt that is never repaid is the relative path to game.html, papered over with a symlink. That is a latent bug dressed as a working-directory problem; embed.FS would end it, and a reader shipping this should fix it before deploying from anywhere but that one folder.
Two sharper threads deserve naming. First, the claim that 'it's often spy tests that start failing because they are usually checking implementation details' is the most useful thing here about testing — a test that breaks whenever you refactor is not protecting you, it is taxing you. Honesty requires noting the GameSpy that follows is still a spy; he narrowed what it asserts rather than escaping mocking. Second, 'This seems too easy! Try and run the application' — suspicion of easiness is a real instinct, and he resolves it by running the thing instead of arguing with it. Likewise 'we have nasty, horrible, working software backed by a test, so now we are free to make it nice' states plainly what red-green-refactor actually buys: permission to be careless first, revoked later.
So what to take away. Don't read this as a WebSocket tutorial; the socket code is thin and you'll look it up anyway. Read it for the dependency-lifetime problem and the io.Writer seam, which will recur every time a per-request destination meets a process-lifetime design. Read the two no-test moments as permission you must earn the same way he does — with a stated cost comparison and a clean commit behind you. And hold the closing line at its actual weight: 'with the TDD flow we have never been very far away from working software' is a claim about recovery time, not correctness. That is a modest promise, and modest promises are the ones worth believing.
Read Build an application with these cards beside each passage.
Open in FeedLitraQuestions and answers: OS Exec
What other standard-library interfaces make good seams?
How Questions and answers unfoldsAI-assisted
Opening: a reader's question as the hook. The chapter doesn't begin with a problem the author invented; it begins with a question posted to Reddit. A reader has a function, GetData(), that shells out via os/exec to produce XML, and wants the test to read from prepared testdata files instead of actually running the command. The reader even proposes a solution — pass a mode parameter so the function knows it's in "test" mode. That proposal is the chapter's real subject, because the author immediately rejects it and offers two guiding claims in its place: difficulty testing is usually a symptom of misplaced separation of concerns, and the fix is dependency injection, not a test flag baked into production code. Having no access to the reader's actual code, the author reconstructs a plausible version of it — a Payload struct, exec.Command("cat", "msg.xml"), a StdoutPipe, an XML decoder, and strings.ToUpper applied to the decoded message — along with the msg.xml file it reads and a first test asserting the uppercased "Happy New Year!" greeting.
Development: locating the seam. The middle section does the analytical work. The author walks through the borrowed standard-library pattern — capture stdout as an io.ReadCloser, Start the command, decode in between, then Wait for the read to finish — and flags the io.ReadCloser as the detail that will matter. Then comes the diagnosis: this code holds two concerns, fetching the raw XML and decoding it plus applying business logic, and they're welded together. The abstraction needed to split them is already present in the code; the io.Reader boundary is where the "seam" sits, so no new interface has to be invented. Before cutting, the author also notes that the existing test will be preserved as an integration test across the two halves — a small methodological point about not losing coverage during a refactor.
Turn and close: the refactor and what it buys. The turn is the rewritten code: GetData now takes an io.Reader and does nothing but decode and uppercase, while a separate getXMLFromCommand owns the exec machinery and hands back a reader over the bytes it read. The old test survives, renamed TestGetDataIntegration, wiring the two together. The payoff is then shown rather than merely asserted: a plain unit test feeds GetData a strings.NewReader holding inline XML (the playful "Cats are the best animal" payload) and checks the uppercased result — no files, no subprocess, no mode flag. The chapter closes by generalizing the win: because GetData no longer cares where its bytes originate, it can be reused with any io.Reader — XML fetched over HTTP, for instance — and the lesson lands as a single line: separate the concerns and lean on Go's existing abstractions, and testing the logic that actually matters becomes easy.
Key claims, with the lines that support them
Trouble writing a test is usually a signal that responsibilities are tangled, not a reason to build a testing switch into the code.
The chapter opens with a reader's question proposing a mode parameter so the function would read a fixture file instead of running the external command. The author declines the flag and reframes the request: the awkwardness comes from one function doing two jobs, and the remedy is to inject the dependency so the production and test paths are the same code path.
“When something is difficult to test, it's often due to the separation of concerns not being quite right”
OS ExecRead in context →
The original function has two separable jobs: obtaining the raw XML and turning it into a result.
Reading the sketched code, the author notes the retrieval half is essentially lifted from the standard library's documentation, while the business logic is the decoding plus uppercasing the message. Since the logic is the part worth testing and it is welded to the command invocation, the two have to come apart.
“The second part is where we have our business logic and by looking at the code we can see where the "seam" in our logic starts; it's where we get our io.ReadCloser.”
OS Exec › Testable codeRead in context →
The right place to cut is where the output becomes an io.Reader.
Following from the two-job observation, the author points out that the pipe hands back a reader, so that type is already the joint in the code. The refactor keeps the command, the Start/Wait pair and the reading of bytes inside a separate retrieval helper, and leaves the decode-and-uppercase function taking only a reader.
“The second part is where we have our business logic and by looking at the code we can see where the "seam" in our logic starts; it's where we get our io.ReadCloser.”
OS Exec › Testable codeRead in context →
2 more claims
A function that accepts an io.Reader becomes both easy to unit test and reusable with other data sources.
The author demonstrates the test by feeding a literal XML payload through strings.NewReader and asserting the uppercased message, with no process launched. The same property applies in production: anything producing a reader works, so the XML could later come from an HTTP response instead of a shell command without touching the logic.
“For example we could start fetching the XML from a URL instead of the command line.”
OS Exec › Testable codeRead in context →
The original end-to-end test is worth keeping once the code is split.
After decoupling, the author retains the test that wires the real command to the logic, relabeling it as an integration test whose job is to confirm the two halves still fit together. It sits beside the fast unit test rather than replacing it, because the unit test deliberately never exercises the command path.
“The second part is where we have our business logic and by looking at the code we can see where the "seam" in our logic starts; it's where we get our io.ReadCloser.”
OS Exec › Testable codeRead in context →
How it works
The 2 mechanisms the chapter’s argument runs on.
Mechanism 1
Hard-to-test code points at a design flaw
When a function mixes fetching data with processing it, a test cannot reach the logic without also triggering the fetch. The author treats that friction as information about the design rather than a testing problem, so the fix is to pass the dependency in as a parameter and let the caller decide where data comes from.
Mechanism 2
Splitting code at an existing interface
The command's output already arrives as an io.ReadCloser, so an interface boundary exists in the code before any refactoring. Changing the function to accept an io.Reader turns that boundary into the split point: the command path still satisfies it, and so does any in-memory reader a test supplies.
Common misreadings
The clean way to use fixture data in a test is to give the function a mode or test flag that makes it read a file instead of shelling out.
Hand the data source to the function as an argument. The caller then supplies a file, a string, or the real command's output, and the function under test runs exactly the same code in both production and tests — no branch exists that only tests take.
“When something is difficult to test, it's often due to the separation of concerns not being quite right”
OS ExecRead in context →
Once the logic can be tested with an in-memory reader, the test that actually runs the external command is redundant.
The author keeps that test around as an integration check. The reader-based test says nothing about whether the command runs, whether its output is read correctly, or whether the two pieces are still wired together — only the end-to-end test covers the seam itself.
“The second part is where we have our business logic and by looking at the code we can see where the "seam" in our logic starts; it's where we get our io.ReadCloser.”
OS Exec › Testable codeRead in context →
Key concepts in Questions and answers
- Dependency Injection
- Pass a dependency in as a parameter so the caller decides where data comes from.
- Where it applies: Works when the dependency can be expressed as a small existing interface like io.Reader.
- Integration tests
- A test that keeps the real command wired up to check the two separated parts still work together.
- Where it applies: Kept alongside, not instead of, the fast unit test of the logic.
- Testable code
- Code that is decoupled and does one thing, so you can feed it inputs directly.
- Where it applies: Judged per concern, not per file.
0 more concepts
Reader’s notes on Questions and answersAI reading companion
“When something is difficult to test, it's often due to the separation of concerns not being quite right”
Read in context →The best line in the chapter, and it reframes the question before answering it. Test pain as a design smell, not a tooling gap.
“Don't add "test modes" into your code”
Read in context →Worth saying why: a test mode means the code you ship is never the code you tested. The branch you exercise is the branch nobody uses.
“these 3 can return errors but I'm ignoring for brevity”
Read in context →Convenient brevity — and the refactored version quietly keeps ignoring them. A chapter about good seams leaves the error seam untouched.
3 more notes
“it's where we get our io.ReadCloser”
Read in context →The seam was already there; the author didn't invent an abstraction, he noticed one the stdlib had handed him. That's the actual lesson.
“Our TestGetData can act as our integration test between our two concerns”
Read in context →Nice move — keep the ugly end-to-end test as a safety net while you cut the code apart. Refactoring with the old test still watching.
“we could start fetching the XML from a URL instead of the command line”
Read in context →Testability sold as a side effect of a good interface rather than the goal. The reuse argument is the stronger one.
A reader’s takeAI reading companion
A tiny chapter doing one thing cleanly: it refuses the question it was asked. The reader wanted a test flag; the answer is that the flag is a symptom. What makes it persuasive is that the fix invents nothing — io.Reader was already sitting there in the original code, and the refactor is mostly a matter of drawing the line where Go had already drawn it. The deep lesson is less 'use dependency injection' than 'learn to see where your code already narrows to a single type, and cut there.' Where it's thin: errors stay ignored in the 'improved' version, which is odd in a book about care; and getXMLFromCommand quietly trades streaming for a full in-memory buffer without a word. Also unexamined is that getXMLFromCommand itself remains untestable — the untestability didn't vanish, it got pushed into a small, boring, obviously-copied-from-the-docs function where it does less harm. That's actually the honest shape of the technique, and saying so would have made the chapter stronger rather than weaker.
Read Questions and answers with these cards beside each passage.
Open in FeedLitraQuestions and answers: Error types
When is a sentinel error value enough instead of a full error type?
How Questions and answers unfoldsAI-assisted
Opening: a question, then a concrete specimen. The chapter begins not with a lesson but with a question lifted from a Slack conversation — if an error is built with fmt.Errorf and interpolated values, can you test it for equality without comparing strings? To make the question tangible, the author invents DumbGetter, a tiny function that fetches a URL and returns its body only on a 200 response. It has two distinct failure paths: the HTTP call itself can fail, or the response can carry a non-200 status. That dual-failure shape is the whole point of the example, since it sets up the situation where a caller might reasonably want to know which thing went wrong. The chapter then writes the naive test the questioner proposed: spin up an httptest server that always answers with "I'm a teapot," call DumbGetter against its URL, and compare err.Error() against a fmt.Sprintf string rebuilt in the test.
Development: listening to the test's discomfort. Rather than pronouncing the test wrong, the author invokes the book's recurring discipline of "listen to your tests" and itemizes why this one feels bad — the test duplicates production's formatting logic, it's tedious to read and write, and the exact wording of the message probably isn't what anyone actually cares about. The pivotal move is to read test ergonomics as a proxy for API ergonomics: if the test can only interrogate the error by inspecting its string, then so can every real consumer of the library, which is both fragile and unpleasant. This reframes the problem from "how do I test this" to the TDD question the book keeps returning to: how would I want to use this code?
The turn: data instead of a string. The answer is a struct, BadStatusError, carrying a URL and a Status — real fields rather than a formatted sentence. The test is rewritten first: it type-asserts the returned error to BadStatusError, fails fast with %T if the assertion doesn't hold, and only then compares the extracted value against an expected struct. A one-line Error() string method satisfies the error interface and keeps the human-readable message, now owned by the type rather than scattered through the function. The test is run and fails informatively — it reports getting *errors.errorString — and the fix is a single substitution in DumbGetter, returning the struct value where fmt.Errorf used to be. The author tallies the gains: the function no longer concerns itself with message formatting, the test now doubles as documentation of how a caller can branch on error kind, and the error remains an ordinary error that can still be logged or passed up the stack.
Closing: the rule, then a modernizing footnote. The wrap-up generalizes the episode into advice — when a function can fail in several ways, don't test by comparing messages; define error types so that both your handling code and your users' handling code can work with structured values. An addendum then updates the technique for Go 1.13 and later: the same test is shown using errors.As, which declares a target variable, returns a boolean for success, and populates the target — the advantage being that it also sees through wrapped errors, where a bare type assertion would not.
Key claims, with the lines that support them
Testing a failure by comparing error message strings is a brittle practice, not just an ugly one.
The author's first test for a non-200 response formats the exact same sentence the function formats, with the same placeholders for URL and status code, and compares it to the returned message. The test therefore restates the implementation rather than the behaviour, is tedious to write and read, and pins down a detail the author argues nobody actually cares about.
“We're constructing the same string as production code does to test it”
Error types › Problems with this way of testingRead in context →
The fix follows from asking how you would want to use the code: have the function hand back a typed error carrying real data.
Instead of a message, the author defines a small struct holding the URL and the status code and gives it an Error method. The rewritten test asserts that the returned error is that type, reports the concrete type it got when the assertion fails, and only then compares the struct against an expected value built from the test server's URL and the teapot status — checking the kind of failure first, its details second.
“Our DumbGetter function has become simpler, it's no longer concerned with the intricacies of an error string, it just creates a BadStatusError.”
Error types › What does the test do?Read in context →
Returning a custom error type makes the producing function simpler and simultaneously documents how callers can handle failures.
The status branch of the getter collapses to a single line that returns the struct with the URL and status filled in, so the function no longer carries any message-formatting logic. The author notes the test now doubles as documentation: anyone wanting more than logging just asserts the type and reads the fields, and because the value still satisfies the error interface it can be logged or passed up the stack like any other error.
“Our DumbGetter function has become simpler, it's no longer concerned with the intricacies of an error string, it just creates a BadStatusError.”
Error types › What does the test do?Read in context →
1 more claim
From Go 1.13 onward, the standard library offers a cleaner way to pull a custom type out of an error value.
The author adds a closing variant of the same test that declares a variable of the custom error type and passes it to errors.As, which reports success with a boolean and populates the variable, so the test can fail with the actual type on mismatch and otherwise compare field values exactly as before.
“In this case we are using errors.As to try and extract our error into our custom type.”
Error types › AddendumRead in context →
How it works
The 2 mechanisms the chapter’s argument runs on.
Mechanism 1
Awkward tests expose awkward APIs
To check a failure, the test had to rebuild the very format string the production code builds, so the assertion duplicated the implementation. That friction is the same friction a caller faces: the only handle on the failure is its message text, so pain in the test is evidence that the API gives callers nothing better to work with.
Mechanism 2
Errors as structs move kinds into types
Giving the failure its own struct with named fields and an Error method keeps it a normal error while making its identity and data visible to the type system. A caller asserts the type to learn which kind of failure happened, then reads the fields directly, so neither side has to parse or reconstruct a message.
Common misreadings
Switching to a custom error type turns errors into a special object you can no longer simply log or return upward.
The struct implements the error interface, so it remains an ordinary error: callers who do not care about the details can propagate or log it as usual, and only those who want to branch on the kind of failure bother to assert the type and inspect its fields.
“Our DumbGetter function has become simpler, it's no longer concerned with the intricacies of an error string, it just creates a BadStatusError.”
Error types › What does the test do?Read in context →
errors.As is just newer syntax for the same type assertion, so which one you use makes no practical difference.
The author presents errors.As as the standard library's own mechanism for extracting an error into a custom type — it signals success with a boolean and fills in the typed variable for you — and introduces it as the up-to-date way to do this work rather than a cosmetic rewrite of the hand-written assertion.
“In this case we are using errors.As to try and extract our error into our custom type.”
Error types › AddendumRead in context →
Key concepts in Questions and answers
- Error type
- A struct carrying error data that implements the error interface, so callers can type-assert instead of reading strings.
- Where it applies: Useful when a function fails for several distinct reasons callers must distinguish.
- Type assertion on error
- Checking at runtime whether an error value is a specific concrete type, then reading its fields.
- Where it applies: Plain assertion misses wrapped errors; errors.As handles the chain.
- Listen to your tests
- Awkward tests are a signal that users of your code will face the same awkwardness.
- Where it applies: A heuristic for design feedback, not proof the code is wrong.
0 more concepts
Reader’s notes on Questions and answersAI reading companion
“is there a way to test equality without comparing the string value?”
Read in context →The whole chapter grows out of one Slack question. Worth noting how often good API design starts as someone's irritation rather than a principle.
“We're constructing the same string as production code does to test it”
Read in context →This is the real indictment. A test that duplicates the implementation's format string proves only that you can copy-paste; it verifies nothing about behaviour.
“The best they can do is look at the error string”
Read in context →The pivot I like: test pain is treated as a leading indicator of consumer pain, not as a testing inconvenience to be worked around.
4 more notes
“Rather than a magical string, we have actual data to work with.”
Read in context →Really this is parsing-vs-structuring: the string version forced users to re-derive data the function already had. Why throw away URL and status only to re-encode them in prose?
“It is still "just" an error, so if they choose to they can pass it up”
Read in context →The quiet payoff of interfaces — you add structure without taking anything away. Nothing downstream needs to know.
“got, isStatusErr := err.(BadStatusError)”
Read in context →Note what the addendum silently admits: the bare type assertion breaks the moment anyone wraps this error with %w. errors.As isn't a stylistic update, it's a correctness fix.
“if got != want”
Read in context →Comparing errors with == only works because BadStatusError is a comparable struct. Add a slice or map field and this line panics at runtime — a hidden constraint the chapter never names.
A reader’s takeAI reading companion
A short chapter doing one move very well: it treats an awkward test as evidence of an awkward API, then fixes the API rather than the test. The argument is strongest where it's least about errors — the claim that writing a test is your first chance to be a user of your own code. What it leaves implicit is as interesting as what it says. Struct equality in the final assertion only works for comparable types; the type assertion in the main body is quietly wrong once errors get wrapped, which the addendum patches without conceding the earlier version was fragile. And there's a scope question the chapter doesn't raise: custom error types are right when callers must branch on the error, but overkill when a single log line is the only consumer. 'Don't compare error strings' is good advice; 'make a type for every error' would not be, and the chapter never quite draws that line.
Read Questions and answers with these cards beside each passage.
Open in FeedLitraQuestions and answers: Context-aware readers
Why use a value receiver for readerCtx's Read method?
How Questions and answers unfoldsAI-assisted
Opening: two small abstractions, introduced separately. The chapter frames itself as a walkthrough of an idea borrowed from Mat Ryer and David Hernandez's blog post, then slows down to make sure both halves of the title make sense. First comes a refresher on io.Reader — a one-method interface over "reading data from something," valuable mainly because conforming to it buys you reuse across the standard library. Then comes the motivating problem: an io.Reader gives no guarantee about how long a read will take, which could be nanoseconds or hours, so pairing it with context's cancellation (covered in an earlier chapter) lets you abort expensive work. The goal is stated as a concrete scenario: a reader holding "ABCDEF" that, cancelled halfway, yields only "ABC" and nothing more.
Development: find the seam, then write a test that proves nothing is broken yet. The author notes a testing difficulty — normally you hand a reader off to something like json.NewDecoder and never touch Read yourself — and resolves it by working directly against the interface: pass a buffer too small to hold everything, cancel, read again, expect zero bytes and an error. Before any production code, a deliberately trivial "happy path" test reads "123456" into a three-byte buffer twice and asserts "123" then "456", purely to get a feel for the mechanics. The TDD cycle proper then begins with the imagined API, NewCancellableReader, and proceeds in the book's characteristic micro-steps: undefined function → a stub returning nil → a nil-pointer panic → the laziest passing implementation, returning the very reader that was passed in. The author anticipates the reader's impatience here, defending the pedantry on the grounds that this test is a guard on ordinary reader behaviour that will stay useful for the rest of the chapter.
The turn: the second test forces the real design. A cancellation test is written by copying the first and inserting a context.WithCancel, a cancel() call between reads, and assertions that the following read errors and returns zero bytes. Each compiler complaint drives the next edit — the signature grows a ctx parameter, a readerCtx struct appears to hold both the context and the delegate reader, the compiler points out the missing Read method, a panicking stub satisfies it, delegation to the wrapped reader restores the happy path, and finally a single guard (if err := r.ctx.Err(); err != nil { return 0, err }) ahead of the delegated call makes both tests pass. Returning the context's own error rather than a generic one is flagged as a deliberate choice: callers can then inspect why cancellation happened.
Closing: the generalizable lesson. The wrap-up lifts three points out of the exercise — small interfaces compose easily; augmenting one thing with another is a job for the delegation pattern (quoted as achieving inheritance-like reuse via composition); and, as a working method, wrap your delegate and first test that it behaves exactly as the delegate does, so you have a safety net while you layer new behaviour on top.
Key claims, with the lines that support them
Readers are worth making cancellable because you never know how long a read will take.
The author points out that the reader abstraction promises nothing about speed: one call might finish in a nanosecond, another might run for hours. Since the work may be expensive and open-ended, an application needs a way to walk away from it, which is exactly what a cancellation signal provides.
“When you're using an io.Reader you have no guarantees over speed, it could take 1 nanosecond or hundreds of hours.”
Context-aware readers › Context aware?Read in context →
Write the test for the API you wish existed and let the compiler's complaints drive each next step.
The author calls a constructor that does not exist yet and works through the errors one at a time: undefined function, then wrong number of arguments once a context is added, then a type that fails to satisfy the reader interface because it lacks a Read method. Each message names precisely the next edit, so the design emerges without guesswork.
“An easy way to start this kind of work is to wrap your delegate and write a test that asserts it behaves how the delegate normally does before you start composing other parts to change behaviour.”
Context-aware readers › Wrapping upRead in context →
When augmenting something, start by wrapping the delegate and proving it still behaves normally.
Before touching cancellation the author ships a wrapper that returns the original reader and keeps the ordinary read-in-chunks test green. He admits this step looks silly and pedantic, but argues the confidence it buys is what makes the later changes safe.
“An easy way to start this kind of work is to wrap your delegate and write a test that asserts it behaves how the delegate normally does before you start composing other parts to change behaviour.”
Context-aware readers › Wrapping upRead in context →
2 more claims
Cancellation is implemented by consulting the context at the top of Read, and returning its error keeps the reason intact.
Building directly on the pass-through wrapper, the author adds a struct carrying the context and the delegate; its Read returns zero and the context's error when one is set, and otherwise delegates. Both the normal-reading test and the stops-when-cancelled test then pass, and because the context's own error is propagated, callers can inspect which kind of cancellation occurred.
“This allows callers of the code to inspect the various reasons cancellation has occurred and this is covered more in the original post.”
Context-aware readers › Write enough code to make it passRead in context →
Small interfaces are valuable precisely because they compose.
The whole feature comes from pairing two modest abstractions — a cancellation signal and a one-method reader — through delegation, which the author describes as getting the reuse of inheritance via composition. Because the wrapper still satisfies the reader interface, it drops into anything that already consumes readers, such as JSON decoding or reading everything into memory.
“This allows callers of the code to inspect the various reasons cancellation has occurred and this is covered more in the original post.”
Context-aware readers › Write enough code to make it passRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Context check before delegating
The wrapper type holds both a context and the underlying reader. Each Read first asks the context whether it is done; if so it returns zero bytes plus the context's own error, otherwise it forwards the call untouched. Cancellation therefore takes effect at the start of the next read, and the caller learns why.
Mechanism 2
Pass-through first as a safety net
The first version of the wrapper simply hands back the reader it was given, so the plain-reading test still passes. That locks in normal reader behaviour as a baseline, so any later break while adding cancellation shows up immediately as a failing test rather than a silent regression.
Mechanism 3
Small buffer exposes a seam
Read fills a byte slice the caller supplies, so a three-byte buffer against six bytes of data forces two separate reads. That gap between the two calls is where the test fires the cancel signal, making mid-stream cancellation observable without any special machinery.
Common misreadings
Handing a context to the reader means a read already underway will be interrupted the moment you cancel.
The context only carries a signal; the wrapper is what acts on it, and it does so by checking at the entry of each Read call. So a read in flight runs to completion, and the cancellation shows up on the following call as zero bytes plus an error.
“This allows callers of the code to inspect the various reasons cancellation has occurred and this is covered more in the original post.”
Context-aware readers › Write enough code to make it passRead in context →
Testing a wrapper that merely returns the thing you passed in proves nothing and can be skipped.
That test pins down the behaviour you must not break. The author keeps it deliberately, noting it verifies ordinary reading still works and supplies the confidence needed while cancellation logic is layered on top.
“An easy way to start this kind of work is to wrap your delegate and write a test that asserts it behaves how the delegate normally does before you start composing other parts to change behaviour.”
Context-aware readers › Wrapping upRead in context →
Key concepts in Questions and answers
- io.Reader
- A one-method interface for pulling bytes into a buffer you supply.
- Where it applies: Gives no guarantee about how long a read takes.
- Context
- A value carried along to signal cancellation to work in progress.
- Where it applies: Only signals; the wrapped code must actually check it.
- Decorator pattern
- Wrapping an object and forwarding calls to it so you can add behaviour around them.
- Where it applies: Requires the wrapper to implement the same interface as the delegate.
0 more concepts
Reader’s notes on Questions and answersAI reading companion
“Supply a fixed-size byte array that doesn't fit all the contents”
Read in context →The whole chapter hinges on this one trick. Reading in small chunks is what makes cancellation observable at all — otherwise there's no gap to cancel in.
“let's just write a "happy path" test where there is no cancellation”
Read in context →A test of the standard library, not of their own code. Pedagogically honest: he's checking his own mental model before coding against it.
“return nil”
Read in context →Returning nil to get a segfault is a deliberately cheap step. The panic is the signal — compile first, then fail loudly, then fix.
4 more notes
“I know, I know, this seems silly and pedantic”
Read in context →The preemptive apology admits the reader is already rolling their eyes. The defense — a regression net for the delegate's normal behavior — is the real lesson of the chapter.
“func (r *readerCtx) Read(p []byte) (n int, err error”
Read in context →Quietly switched from pointer receiver to value receiver mid-chapter, while the constructor still returns &readerCtx. It compiles, so nobody notices — but it's an unremarked inconsistency in a book about going slowly.
“if err := r.ctx.Err(); err != nil”
Read in context →Worth naming the limit: this only checks between reads. A Read already blocked for hours won't be interrupted. The chapter sells cancellation but delivers polling.
“the delegation pattern is an object-oriented design pattern”
Read in context →A quoted OO definition parachuted into a Go chapter. Go reaches delegation not by design choice but because it has no inheritance to choose instead.
A reader’s takeAI reading companion
A short chapter whose real subject isn't context or readers but TDD tempo: each step is almost insultingly small — return nil, get a segfault, return the input unchanged, add a panicking method — and the author knows it, apologizing in advance for the pedantry. The payoff he argues for is real: wrapping a delegate and first asserting it still behaves exactly like the delegate gives you a regression net before you change any behavior. The technical content is thin by design, essentially fifteen lines borrowed from Ryer and Hernandez, and the chapter is upfront about that. Two things go unexamined. First, the receiver quietly shifts from *readerCtx to readerCtx without comment, in a text that otherwise preaches letting the compiler guide you line by line. Second, and more substantively, checking ctx.Err() at the top of Read only cancels between reads — a Read that blocks for hours, precisely the scenario used to motivate the whole exercise, is untouched. The chapter promises cancellation of slow readers and ships cooperative polling. That gap is probably the most useful thing a sharp reader takes away, and it is the one thing the wrap-up doesn't mention.
Read Questions and answers with these cards beside each passage.
Open in FeedLitraQuestions and answers: Revisiting HTTP Handlers
How would you give different error types different status codes instead of a blanket 500?
How Questions and answers unfoldsAI-assisted
The chapter opens by framing itself as a sequel rather than a repeat: the book has already covered testing an HTTP handler, so this installment is about design instead. The author states a thesis up front — difficulty testing handlers is rarely a testing problem, it's a design problem — and compresses it into the line he says he repeats throughout the book: if tests hurt, treat the pain as a signal about the code. Single responsibility and separation of concerns are named as the cure, with interfaces and dependency injection as the mechanism.
From there it moves to evidence. A reader's question — how to test a handler with a MongoDB dependency — supplies a real handler, and the author simply enumerates what that one function does: writes responses and status codes, decodes the body, opens a database connection, queries and applies business rules, hashes a password, inserts a record. The verdict is a single line: too much. The chapter then steps back from the specific code to ask what a handler is for, and offers a three-part answer the author claims holds in nearly every project he's worked on: parse and validate the request, hand the data to some service, translate the service's result into a response. He lists the payoffs — narrow handler tests, business logic testable without HTTP, logic reusable elsewhere, handlers insulated from changes behind a stable interface.
The turn is a deliberate deflation of mystique. http.HandlerFunc is quoted in full — a type over a two-argument function — and the reader is invited to notice there's no framework magic, just a function, and we already know how to test functions. A minimal "teapot" handler and its test demonstrate the whole mechanism: call the function, pass a recorder as the writer, assert on what was recorded. The author then answers the standard objection that TDD examples are too simple by inverting it — wouldn't it be nice if real code read this simply? — and redesigns the registration handler accordingly: a UserService interface with a Register method, a UserServer struct holding it, and a handler that decodes, delegates, and maps outcomes to 400, 500, or 201. He is candid about the shortcuts, flagging the blanket 500 as a bad API and leaving a TODO for differentiated error types.
The tests then serve as proof of the claim. A hand-written MockUserService with an injectable function and a record of registered users supports three small subtests: a valid registration (status, body, and the user actually passed through), invalid JSON, and a failing service. Anticipating the accusation of cheating — where did the database go? — the author says the omission is the point: freeing the handler of persistence also frees persistence from HTTP, so each can be tested on its own terms. A stubbed MongoUserService and a four-line main show the two halves snapping together only at the edge of the program.
The closing argument is about the future rather than the present. Add a confirmation email later, and the old handler and its tests would both have to change — which is how code quietly becomes unmaintainable, each new concern accreting onto the one function designed to hold everything. Behind an interface, the handler needn't be touched at all. The wrap-up restates the opening thesis as a diagnosis of a cultural habit: people treat handlers as special and suspend their normal engineering judgment, and that, not Go's HTTP package, is what makes them hard to test. Write them like ordinary functions with clear responsibilities, and testing takes care of itself.
Key claims, with the lines that support them
When code is hard to test, the problem is usually its design rather than the testing itself.
The author opens with a reader's question about testing a handler that depends on MongoDB, then lists everything that single function does: write headers and status codes, decode the body into a user, dial a database, query it and apply business rules, hash a password, and insert a record. The verdict is that this is simply too much for one function — the test pain is a symptom of that pile-up, and it sets up the redesign that follows.
“If your tests are causing you pain, listen to that signal and think about the design of your code.”
Revisiting HTTP HandlersRead in context →
A handler should do three things: parse and validate the incoming request, hand the data to a service, and choose a response based on what comes back.
A minimal teapot handler shows the response half: the test just calls the function with a fake request and a recorder and asserts on the recorded status code. The rewritten registration handler shows the full shape — decode the body, call the service's Register method, then answer 400 on a bad payload, 500 if the service errors, and 201 with the new ID on success. The author treats this as a strong default rather than a law, saying it has fit roughly ninety-nine cases out of a hundred.
“Call some ServiceThing to do ImportantBusinessLogic with the data I got from step 1.”
Revisiting HTTP Handlers › What is a HTTP Handler and what should it do ?Read in context →
Once the dependency sits behind an interface, the handler's tests become small and fast.
A fake user service records the users passed to it and returns whatever the test wants, so three short subtests cover the handler's whole job: a valid payload yields 201 with the ID and one recorded user, garbage text yields 400 without any service at all, and a failing service yields 500. No database, network, or fixtures are needed because the handler no longer knows how registration happens.
“By separating concerns using an interface we don't have to edit the handler at all because it's not concerned with the business logic around registration.”
Revisiting HTTP Handlers › A more robust and extensible design with little effortRead in context →
3 more claims
Keeping database work out of the handler also makes the persistence layer easier to work with.
The author writes the Mongo-backed service as a separate type with its own constructor and Register method, left unimplemented on purpose, and notes it can be tested on its own. Main then snaps the two halves together: build the Mongo service, pass it into the server, and serve the handler method. Because the storage code carries no HTTP details, its tests do not have to simulate requests or responses.
“By separating concerns using an interface we don't have to edit the handler at all because it's not concerned with the business logic around registration.”
Revisiting HTTP Handlers › A more robust and extensible design with little effortRead in context →
This separation makes the system cheaper to change later, not just easier to test today.
The author imagines a likely next requirement — emailing new users a confirmation. Under the original design that change lands inside the handler and forces its tests to be rewritten, which is how handlers gradually absorb everything and rot. With registration behind an interface, the new behavior goes into the service and the handler needs no edits at all, as long as the interface's shape holds steady.
“By separating concerns using an interface we don't have to edit the handler at all because it's not concerned with the business logic around registration.”
Revisiting HTTP Handlers › A more robust and extensible design with little effortRead in context →
The complaint that tutorial examples are unrealistically simple misses the point of the exercise.
The author's reply is to turn the objection around: it would be good if production code read and tested as easily as the toy examples, and that outcome is achievable — though not effortless — by practicing the same engineering principles. The redesigned registration code is offered as the demonstration that an ordinary real-world endpoint can end up looking that simple.
“If your tests are causing you pain, listen to that signal and think about the design of your code.”
Revisiting HTTP HandlersRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Handlers are just functions
Go's handler type is only a function taking a response writer and a request, with no framework hooks or annotations involved. Because of that, a test can call it directly and pass a fake request plus an in-memory recorder, then inspect the recorded status and body — testing it is no different from testing any other function.
Mechanism 2
Injecting a narrow interface
Instead of opening a database inside the handler, the server type holds a service expressed as an interface and receives it through its constructor. The handler then only calls a method, so a test can supply a recording stand-in that returns canned results, and the real database implementation can be swapped in at startup without touching handler code.
Mechanism 3
Concerns split both ways
Pushing registration logic behind a service leaves the handler with parsing and responding, and leaves the business and persistence code free of response writers and status codes. Each side can then be exercised on its own terms: the handler with fast in-memory tests, the storage layer against a real database.
Common misreadings
HTTP handlers are a special kind of code that needs a dedicated testing framework or elaborate scaffolding.
They are ordinary functions, and the standard library already supplies everything a test needs: one helper builds a fake request, another records whatever the handler writes, and the test just calls the function and checks the recording. The author argues people get into trouble precisely because they treat handlers as special and abandon normal design habits when writing them.
“Call some ServiceThing to do ImportantBusinessLogic with the data I got from step 1.”
Revisiting HTTP Handlers › What is a HTTP Handler and what should it do ?Read in context →
The redesigned handler's error handling is the pattern to copy — bad JSON gets 400 and anything else gets 500.
The author flags the blanket 500 as a stand-in chosen for brevity and says outright that returning it for every error makes a terrible API; he also considers the original code's habit of answering 400 for everything wrong. The intended lesson is the separation of concerns, with more discriminating error handling — for instance distinguishing error types — left as a later refinement.
“Call some ServiceThing to do ImportantBusinessLogic with the data I got from step 1.”
Revisiting HTTP Handlers › What is a HTTP Handler and what should it do ?Read in context →
Key concepts in Questions and answers
- Handler
- In Go, a plain function taking a ResponseWriter and a Request — no framework magic, so it is testable like any function.
- Where it applies: It should only parse/validate input, call a service, and write a response.
- Dependency Injection
- Pass the service the handler needs into its constructor instead of building the database connection inside the handler.
- Where it applies: Works when the dependency can be expressed as a narrow interface.
- Interface
- A named set of methods the handler calls without knowing the concrete implementation behind it.
- Where it applies: Decoupling holds only while the interface shape stays stable.
- Mocking
- A stand-in service, here MockUserService, that records calls and returns canned results so handler tests stay fast.
- Where it applies: Only checks the handler's own responsibilities, not real persistence.
- httptest
- Standard library helpers: NewRequest builds a fake request and NewRecorder spies on what the handler wrote.
- Where it applies: Records the response in memory; no real network involved.
0 more concepts
Reader’s notes on Questions and answersAI reading companion
“If your tests are causing you pain, listen to that signal and think about the design of your code.”
Read in context →The whole chapter is an unfolding of this one line. Testing pain as diagnostic rather than chore — that reframing is the actual lesson; the Mongo refactor is just the demo.
“ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)”
Read in context →Note the discarded cancel func — a context leak sitting quietly in the sample. The author never mentions it, because his lens is structural, not line-level. A different reviewer would have flagged this first.
“The lines can blur depending on how abstractly you're thinking and sometimes your first guess might not be right.”
Read in context →Rare honesty in a principles chapter. He admits SRP has no objective unit — then immediately asserts he does know for handlers. The confidence is empirical, not derived.
5 more notes
“It's just a function, and we know how to test functions.”
Read in context →The demystifying blow. No DI container, no test client, no framework harness — Go's handler signature is deliberately boring, and that boringness is the feature.
“I must stress that returning 500 for all errors makes for a terrible API!”
Read in context →He names his own shortcut rather than hiding it, which keeps the example honest. But error classification is exactly where handler/service boundaries get messy in practice — the hard part is deferred to a todo.
“server := NewUserServer(nil)”
Read in context →Passing nil as the service is the test quietly proving a claim: bad JSON never reaches the dependency. Elegant, and it would panic loudly if the design regressed.
“What about the database code? You're cheating!”
Read in context →He voices the objection before the reader can — then answers by moving the problem, not solving it. Fair, but the real complexity (connection pools, transactions, duplicate-key semantics) now lives in an unwritten MongoUserService.
“we'd want to email the user a confirmation of registration”
Read in context →The extensibility argument cuts both ways: the handler stays untouched, but UserService.Register now silently does two things. The god-object pressure migrates down a layer rather than dissolving.
A reader’s takeAI reading companion
A short, polemical chapter whose real subject isn't HTTP at all — it's the claim that untestable code is badly designed code, with handlers as the convenient crime scene. Its strength is the surgical read of the original function: six responsibilities enumerated flatly, which does more rhetorical work than any argument about SRP. Its structural move is interface-plus-DI, and the payoff is genuine — the three tests at the end are almost embarrassingly simple, and NewUserServer(nil) is a small piece of craft. But the chapter wins partly by choosing its ground. Everything genuinely difficult — error taxonomy, connection lifecycle, what happens when registration grows a second and third obligation — is pushed behind the UserService boundary and marked todo.
The author knows this; 'You're cheating!' is his own voice. So the honest reading is that complexity is relocated to where it can be tested in isolation, not reduced. That is still a large win, and the chapter's final insistence that handlers are 'just functions' is the right antidote to treating web entry points as a special magical category exempt from ordinary engineering judgment. Worth noting too that the sample's own context-leak goes unremarked — a reminder that a design lens, however sharp, is also a filter.
Read Questions and answers with these cards beside each passage.
Open in FeedLitraMeta: TDD Anti-patterns
How would you test the UserService itself once the rules move out of the handler?
How Meta unfoldsAI-assisted
Opening: framing TDD's difficulty as a design problem. The chapter begins with a short premise rather than a definition — that practitioners should periodically audit their own habits — and immediately makes a distinction that governs everything after it: TDD itself is conceptually simple, and the pain people attribute to it is really the pain of design. From there the author moves to the baseline case, not doing TDD at all, conceding that good software can be written without it while arguing that the red-green-refactor cycle supplies a formal way to decompose problems that ad-hoc work lacks. The next few items stay close to mechanics and to misreadings of the cycle: the pedantic belief that refactoring requires its own new test first (the real rule being that you may change anything but behaviour while green), and then a cluster of symptoms that all trace back to skipping or rushing the red step — tests that can never fail, assertions whose failure messages say nothing more than that false isn't true, and assertions that compare whole complex objects when the test only cares about a single field. Each is diagnosed the same way: you did not write a test and watch it fail attentively enough.
Development: from mechanics to the central thesis, "listen to your tests." The piece then widens from technique to interpretation. After warning against piling many assertions into one scenario (with subtests offered as the Go-specific remedy, and an acknowledgement that slow acceptance tests force a trade-off), it pivots on a quoted line from Dave Farley — that TDD gives the fastest possible feedback on design — and states the thesis the rest of the chapter elaborates: difficulty in testing is difficulty in use, so treat the test as your code's first user. Everything following becomes an application of that principle. Heavy setup and swarms of test doubles are read as reports about dependency count and coupling, not as mere inconvenience. Large interfaces are treated as leaky abstractions, and the Go proverb about big interfaces yielding weak abstractions supports an argument against interface pollution: export a struct with its methods and let clients declare the narrow interfaces they actually need, reserving exported interfaces for small coherent sets or genuinely decoupled implementations. A short aside on test-double taxonomy urges stubs over mocks and discourages spying on implementation detail.
The turn: a worked example that argues the design, not the test. The chapter's hinge is the registration-handler example, where the abstract advice is cashed out in code. A handler taking a UserStore and an Emailer looks defensible at two dependencies, so the author re-reads it by listing responsibilities and marking which ones belong to HTTP and which do not — parsing requests and returning status codes qualify, while existence checks, storage, and email composition are flagged as questionable. The pressure is then applied through hypothetical requirements (translations, SMS) and through a set of rhetorical questions: should a business rule about sending email live in a handler, and should verifying it require constructing requests and reading responses? The resolution is a single UserService dependency, which makes the handler trivially testable and isolates registration rules from transport. This is the chapter's demonstration that the discomfort felt while writing tests was information about structure.
Closing: encapsulation, tables, and a summary that returns to design. Two shorter anti-patterns follow in the same register. Exporting a function merely to test it is treated as a design regression that misleads maintainers and ends with people debugging code nothing but tests call; the Go-flavoured countermeasure is to test from a consumer's vantage point, using an external _test package as a compile-time barrier against coupling to internals. Complicated table tests get a deliberately absurd struct — coordinates alongside leap years and full moons — to show what happens when unrelated scenarios are forced into one table, and the advice is to start a new test instead of appending another boolean, under the maxim that simple is not easy. The summary then closes the loop opened in the first lines: nearly all unit-test problems reduce to not following the process or to poor design, TDD's purpose is feedback on that design, and the repeated instruction — listen to your tests, because they reflect your design back at you — is restated as the chapter's final takeaway, with the promise that honesty about test quality makes you a better developer.
Key claims, with the lines that support them
TDD itself is easy to describe; what feels hard about it is the design work it exposes.
The author points to the loop's structure as the payoff: red forces you to state what you want, green gets it working, refactor is where you make it right. Teams without that formal breakdown end up working ad hoc, which he argues makes engineering harder than it needs to be rather than easier.
“Don't mistake this for TDD being hard, it's design that's hard!”
TDD Anti-patternsRead in context →
Following the red step strictly and actually reading the failure message prevents most bad unit tests.
Two symptoms are offered as proof of what skipping it costs: a failure that says only that false differs from true, which tells a debugger nothing about what broke; and asserting on an entire complex object when the test cares about one field, which welds the test to data it never needed. Both disappear if you insist on seeing the test fail informatively first.
“Write a test, see it fail (and don't be ashamed of the error message)”
TDD Anti-patterns › Useless assertionsRead in context →
Your tests are the first consumer of your code, so difficulty testing is evidence of difficulty using.
The author invokes the experience of opening a test with twenty, fifty or two hundred lines of setup before anything interesting happens, and reads it as a plain equation: complicated tests mean complicated code. The remedy is to ask why the code has to be that complicated instead of tolerating the setup.
“Simply put, if testing your code is difficult, then using your code is difficult too. Treat your tests as the first user of your code and then you'll see if your code is pleasant to work with or not.”
TDD Anti-patterns › Not listening to your testsRead in context →
3 more claims
Pulling scattered dependencies behind one meaningful service keeps business rules away from transport code and makes both easier to test.
Building on the previous point, he walks through a registration HTTP handler that looks fine with only two dependencies, then lists its real responsibilities and finds it owns rules about duplicate checks, storage and confirmation email. Adding email translations or an SMS would mean editing a web handler, and verifying the rule requires manufacturing HTTP requests; replacing both dependencies with a single Register call shrinks the handler's test and isolates the rules. It is framed as re-evaluating a design, not a rule that two dependencies are always too many.
“Changes to the rules around registration are isolated away from HTTP, so they are also simpler to test”
TDD Anti-patterns › Consolidate dependenciesRead in context →
Many assertions crammed into one test are usually a design problem, not a reading problem.
He observes that extra assertions pile up precisely when setup is horrible and nobody wants to reproduce it for a second test, so the fix is to repair whatever makes new assertions hard; one assertion per test, with subtests where you need more, keeps behaviour separate from implementation detail. The stated exception is tests whose setup or run time is expensive, such as browser-driven acceptance tests, where slower debugging may be the better trade.
“Simply put, if testing your code is difficult, then using your code is difficult too. Treat your tests as the first user of your code and then you'll see if your code is pleasant to work with or not.”
TDD Anti-patterns › Not listening to your testsRead in context →
Default to testing your package the way an outsider would use it, rather than widening its surface for tests.
Exporting a function just to reach it from a test is described as making the design worse and misleading maintainers, with a concrete endgame: someone debugging eventually discovers the function under test is called from nowhere but tests. Placing tests in a separate test package turns the restraint into a compile-time limit, since only exported members are reachable.
“Simply put, if testing your code is difficult, then using your code is difficult too. Treat your tests as the first user of your code and then you'll see if your code is pleasant to work with or not.”
TDD Anti-patterns › Not listening to your testsRead in context →
How it works
The 3 mechanisms the chapter’s argument runs on.
Mechanism 1
Why the failing step matters
Watching a brand-new test fail proves it can fail at all, and forces you to read the message it produces. Skip that step and you get tests written after the code that no longer guard anything, plus failures that only report that one boolean differed from another.
Mechanism 2
Test doubles as a design gauge
Every stand-in a test needs stands for a real dependency, and every scripted interaction stands for a call the code makes. So a test needing many doubles and long setup is measuring how entangled the production code is, which is why painful tests predict painful usage.
Mechanism 3
Big interfaces push cost onto users
Export a wide interface and anyone testing against it must implement every method, including ones they never call, often stubbing them with panics. Exporting a plain struct instead lets each caller declare the narrow interface it actually needs, which is why the standard library keeps its interfaces tiny.
Common misreadings
While refactoring you may not introduce a new abstraction, such as extracting a struct, until you have written a test for it.
With the tests green you can restructure the code however you like; the single restriction is that you must not add or alter behaviour, and new behaviour is what requires a new failing test first. The freedom to rearrange is the whole reason the tests exist.
“You can do whatever you like to the code when the tests are green, the only thing you're not allowed to do is add or change behaviour.”
TDD Anti-patterns › Misunderstanding the constraints of the refactoring stepRead in context →
Since table tests are idiomatic, the tidiest approach is to grow one table and add a field or boolean whenever a new scenario appears.
Tables earn their keep only when the setup is identical and the inputs vary; once unrelated conditions are bolted on as extra flags the table becomes hard to read and debug. Writing a separate test is the better move, because adding a field is easy while keeping the suite simple is a different thing.
“Don't be afraid to break out of your table and write new tests rather than adding new fields and booleans to the table struct.”
TDD Anti-patterns › Complicated table testsRead in context →
Key concepts in Meta
- Refactoring
- With green tests you may restructure code freely; the single prohibition is adding or changing behaviour.
- Where it applies: Applies only while tests pass; new behaviour still requires a new failing test first.
- Interface pollution
- Exporting a huge interface forces clients to stub methods they never use; expose a struct and let them declare the small interface they need.
- Where it applies: Export an interface only when it is small and coherent, or when implementation genuinely must be decoupled.
- Evergreen tests
- Tests that cannot fail in the way they claim to protect against, typically written after the code or to chase coverage.
- Where it applies: Nearly impossible if you always watch a new test fail first.
- Test Double
- Stand-ins for dependencies; many doubles or heavy mock setup signals too many dependencies and interactions in the design.
- Where it applies: Mocks and spies are occasionally useful but couple tests to implementation detail; prefer stubs and behaviour.
- Table driven tests
- One shared setup with varying inputs; they degrade when unrelated scenarios are forced in as extra boolean fields.
- Where it applies: Good only when setup is identical and just inputs vary; otherwise write separate tests.
- Encapsulation
- Keeping a small exported surface; testing from a separate test package stops tests coupling to internals.
- Where it applies: Making things public merely to test them worsens design and can leave functions only called by tests.
0 more concepts
Reader’s notes on MetaAI reading companion
“Don't mistake this for TDD being hard, it's design that's hard!”
Read in context →Convenient framing: any failure of TDD gets reassigned to the developer's design skill, which makes the method unfalsifiable. Still mostly true, but notice the move.
“You can do whatever you like to the code when the tests are green, the only thing you're not allowed to do is add or change behaviour”
Read in context →The cleanest sentence in the chapter. The whole refactor rule collapses into one line, and the pedant in the anecdote is refuted by definition rather than argument.
“there are no scenarios where this test can fail”
Read in context →Evergreen tests are worse than no tests — they cost maintenance and sell false confidence. Coverage metrics actively reward producing them.
9 more notes
“false was not equal to true”
Read in context →Worth noting the real diagnosis isn't the assertion library, it's that nobody ever watched this test fail. The message is a fossil of a skipped red step.
“Treat your tests as the first user of your code”
Read in context →The strongest idea here. But it has limits the author doesn't admit: tests are an unusually invasive user, and sometimes test pain reflects testing-tool weakness, not design rot.
“When you have lots of test doubles in your tests, that means the code you're testing has lots of dependencies”
Read in context →True, yet mock count also tracks how you test. Over-mocking is often a testing-style choice masquerading as a design verdict.
“the bigger the interface, the weaker the abstraction”
Read in context →Go-specific but the deeper point is ownership: in Go the consumer declares the interface, so the author's job is to not pre-decide it for them.
“Does it feel right that the important rule of "we should send an email" resides within a HTTP handler?”
Read in context →The refactor to UserService doesn't remove the complexity, it relocates it — now UserService has all five responsibilities. The gain is real but it's testability and isolation, not less work.
“the function being tested is only called from tests”
Read in context →Grimly funny and very recognisable. Encapsulation leaks of this kind are permanent; the test that forced it is usually long forgotten.
“AtWarWithEurasia bool”
Read in context →The Orwell joke lands because every bloated table test does end up with a field like this — a boolean smuggling in a whole second scenario.
“Simple is not easy”
Read in context →Borrowed from Rich Hickey and doing a lot of load-bearing work here uncredited. Still the best one-line defence against the "just add a field" instinct.
“listen to your tests, they are reflecting your design back at you”
Read in context →Repeated maybe five times in one chapter. The repetition signals the author knows this is the single transferable lesson — the anti-pattern list is just evidence for it.
A reader’s takeAI reading companion
A practitioner's list, not a theory, and it's honest about that. The real spine isn't the catalogue of anti-patterns but a single claim repeated until it sticks: test pain is design feedback, so stop numbing it. Nearly every item is a corollary — useless assertions and evergreen tests come from skipping red; mock sprawl, leaky interfaces and the registration-handler example come from dependency tangles; broken encapsulation and monstrous table tests come from treating tests as an afterthought to satisfy rather than a design client to serve. The chapter's weak spot is where it's most confident: by attributing all failures to 'developers not following the process' or 'poor design', it makes TDD itself immune to criticism, and it never concedes that a test can be painful because testing tools are bad or because the boundary being crossed is genuinely expensive (databases, browsers, time).
The handler refactor is the clearest instance — complexity is relocated behind UserService, which buys isolation and cheap HTTP tests, but the chapter presents it as if complexity were removed. Best lines: the refactoring permission rule (do anything, change nothing observable), the test-as-first-user reframe, and 'simple is not easy'. Most useful concrete takeaways for Go specifically: test from an external _test package so coupling to internals becomes a compile error, and expose structs rather than interfaces so consumers declare the narrow abstraction they actually need.
Read Meta with these cards beside each passage.
Open in FeedLitraWhere the argument strains
Not a rebuttal — the places a careful reader will want to push.
- Stubs and spies are condemned as carriers of unvalidated assumptions, yet the application chapters build the HTTP server with exactly a stub store and a spy to break the GET/POST deadlock.
“If there is a discrepancy between the behaviour of the double and the real dependency, or if one happens over time (e.g. the real dependency changes, which has to be expected), you may have passing tests but failing software.”
Working without mocks, stubs and spies › The problem with stubs and mocksRead in context → - Refactoring is defined as change that never touches tests, yet the discovery-driven TDD the book teaches repeatedly reshapes a design's seams and therefore its tests.
“You're doing something else if you are "refactoring" some code and having to change tests at the same time.”
Refactoring step, starting checklist › Refactoring vs other activitiesRead in context → - Hard-coding or sliming to escape red is endorsed while crude implementations are warned to become unmaintainable, and no rule is given for when the slime must go.
“I know it feels weird just to hard-code the result but with TDD we want to stay out of "red" for as long as possible.”
Roman Numerals › RefactorRead in context → - Tests are simultaneously the enabler of safe change and a cost to minimise, and acceptance tests are both the north star and expensive and design-coupled, so the right quantity of each is left unresolved.
“For this reason, it is foolish to only rely on acceptance tests. They do not have many of the qualities unit tests have, and a system with a large number of acceptance tests will tend to suffer in terms of maintenance costs and poor lead time.”
Introduction to acceptance testing › Potential drawbacks vs unit testsRead in context →
4 more tensions and 8 limits of the evidence
- Testing only the public API is urged, yet compiler errors and implementation-level choices such as pointer receivers or strings.Builder repeatedly drive the next test, blurring where the behavioural boundary lies.
“Favour testing behaviour rather than implementation detail”
Why unit tests and how to make them work for you › Favour testing behaviour rather than implementation detailRead in context → - Reflection and interface{} are taught in detail and then recommended against, and channels, mutexes and atomics are each offered as the fix for shared state with no decision rule beyond single-value versus multi-field.
“As a user of a function that takes interface{} you lose type safety.”
Reflection › So why not use interface{} for everything and have really flexible functions?Read in context → - Disciplined minimal TDD let a plausible but wrong cancellation implementation pass until an extra assertion was added, showing the loop does not by itself guarantee a complete suite.
“By being disciplined it highlighted a flaw in our tests and this is a good thing!”
Context › Write enough code to make it passRead in context → - Separating specifications from drivers and extracting shared abstractions adds indirection, which sits uneasily with the refactoring advice that clarity comes from removing structure rather than adding it.
“Decoupling how the specification is executed allows us to reuse it in different scenarios.”
Learn Go with Tests - Scaling Acceptance Tests (and light intro to gRPC) › Testing on steroidsRead in context →
Limits of the evidence
- Every lesson is demonstrated on deliberately trivial exercises — Hello, Sum, Wallet, Dictionary, a clock face, one small poker app — so claims about large systems or team constraints rest on argument rather than demonstration.
“Those who choose not to write tests will typically be reliant on manual testing. For anything other than a small project this will be a tremendous time-sink and does not scale in the long run.”
Why unit tests and how to make them work for you › When refactoring code you must not be changing behaviourRead in context → - Performance figures come from the author's single machine, a 20ms sleeping stub rather than live HTTP calls, and one template benchmark the author concedes is usually negligible.
“Strings in Go are immutable, meaning every concatenation, such as in our Repeat function, involves copying memory to accommodate the new string.”
Iteration › BenchmarkingRead in context → - The arguments that test doubles and bad design cause bad tests rest on anecdote — one six-API story, a database-pool illustration, reader snippets explicitly described as not a survey.
“If there is a discrepancy between the behaviour of the double and the real dependency, or if one happens over time (e.g. the real dependency changes, which has to be expected), you may have passing tests but failing software.”
Working without mocks, stubs and spies › The problem with stubs and mocksRead in context → - Key concurrency evidence is nondeterministic: the concurrent-map-writes failure is intermittent, and the synctest findings depend on specific runtime scheduling that may not generalise.
“Go maps are not safe for concurrent writes, so the runtime throws a fatal error to prevent memory corruption.”
Concurrency › ... and we're back.Read in context → - Several taught solutions are left acknowledged as rough: prefix-trimming routing conceded fragile, a timing sleep in the WebSocket test, and binaries run from different directories silently creating two database files.
“This test has forced us to actually look at the request's URL and make a decision.”
HTTP Server › Write enough code to make it passRead in context → - Guarantees are partial or circumstantial: Testable Examples only execute with an Output comment, exact float comparison is safe only because the chosen values are whole numbers, and unchecked errors are caught only by the external errcheck linter.
“Often code examples that can be found outside the codebase, such as a readme file, become out of date and incorrect compared to the actual code because they don't get checked.”
Integers › Testable ExamplesRead in context → - All guidance is bound to Go's specific surface and toolchain versions — modules, io.Reader, context, errors.As, httptest, editor shortcut lists — leaving transfer to other languages untested.
“Using Modules is pretty straightforward. Select any directory outside GOPATH as the root of your project, and create a new module with the go mod init command.”
Install Go, set up environment for productivity › Go ModulesRead in context → - Notable conclusions are judgement calls rather than demonstrated results: that sites need no SPA, that the HTML was not worth testing, that a test folder is the wrong trade, and that context goes first because Google does it.
“Many websites do not need to be an SPA. HTML and CSS are fantastic ways of delivering content and you can use Go to make a website to deliver HTML.”
HTML TemplatesRead in context →
Mind map of the whole book
5 clusters of ideas: a plain-English label, a one-line explanation, and the term the text itself uses.
1 · Fail first, pass cheap, tidy up
- Write a failing test, pass it crudely, then improve it
- Write a failing test, do the least work to pass it — even hard-coding or 'sliming' a return value — then refactor; grown outside-in from a steel thread or acceptance test and kept red for as little time as possible.In the text: TDD cycle
- Reshape the code's insides while the tests keep passing
- Reshaping code for clarity with behaviour unchanged, so tests ideally need no edits; larger, partly subjective structural change is 'big design' and must itself be driven in small test-backed steps.In the text: Refactoring
- One test loops over a list of named cases
- One test body looping with subtests over a slice of named input/expected cases, so new scenarios cost almost no boilerplate — but degrading when unrelated cases are forced in as extra flags.In the text: Table driven tests
- Check a rule holds for many inputs, not a few picks
- Tests that state a domain truth — the Roman subtractor rule, the unit circle — and let a library throw many generated inputs at it, with your own generator keeping those inputs inside the domain.In the text: Property-based tests
2 · Test from outside, pass deps in
- If a test is painful to write, the design is off
- Difficulty writing a test is read as evidence of poor separation of concerns or awkward ergonomics for callers, so the fix belongs in the production design rather than in test-only modes.In the text: Testability as a design signal
- Name only the methods a caller needs, so parts swap out
- A named set of just the methods a caller needs, satisfied implicitly, so a dependency can be substituted; keep it small, and invent one only when something else will really be passed.In the text: Interface as a seam
- Hand collaborators in from outside instead of building them
- Passing collaborators in from outside instead of constructing them inside or reaching for globals, so tests supply fakes while production supplies the real thing.In the text: Dependency injection
- Take the standard stream types so any source plugs in
- The standard one-method IO abstractions; because files, os.Stdout, http.ResponseWriter, WebSocket connections and strings.NewReader all satisfy them, one body of code serves tests and every front end.In the text: io.Writer and io.Reader
3 · Fakes and whole-system tests
- Scripted stand-ins that can lie about the real thing
- Any controllable stand-in for a real dependency, from a dummy filling a parameter slot to a spy that records calls, durations and ordering so otherwise invisible behaviour can be asserted.In the text: Test double (mock, stub, spy)
- A simple working stand-in, kept honest by a shared test
- A genuine, stateful, usually in-memory implementation of a dependency, trusted because one reusable test suite written against the interface runs against both it and the real thing.In the text: Fake verified by a contract
- Boot the whole thing and drive it like a real user
- A black-box test that boots and drives the whole system from the outside as a user or business would, passing only when the user-visible feature works end to end.In the text: Acceptance test
- Keep the rules separate from how you poke the system
- Splitting essential domain complexity into an implementation-free specification from accidental complexity — networks, transports, APIs — handled by a driver that adapts it to a particular system.In the text: Specification and driver
4 · Put correctness in the types
- Return failures as values you can inspect and match
- Failure signalled by returned values — sentinel errors compared with errors.Is, structs carrying data inspected with errors.As, wrapped with %w to add context without losing the cause.In the text: Error as an inspectable value
- Shape types so bad values can't be built at all
- Structs, names over existing types carrying their own methods, and generic type parameters, so values outside the domain cannot be expressed — the opposite of interface{}'s runtime-only checking.In the text: Types that exclude invalid values
- Lean on Go's shipped packages instead of your own
- The shipped packages — ServeMux, httptest, JSON, html/template with go:embed, time, os/exec — presented as sufficient for both the application and its tests, needing no framework.In the text: Go standard library
5 · Concurrency and speed need proof
- Time the code before and after to prove it got faster
- A function whose loop the framework times, used to measure before and after a change so performance claims become measurements rather than assertions.In the text: Benchmark
- Run work separately and pass results over channels
- Separate processes started with
go, often anonymous functions, communicating by sending and receiving values; select waits on several channels and runs whichever is ready first.In the text: Goroutines and channels - Two threads touching one value need a lock
- Concurrent conflicting access to shared memory, found with the -race detector and fixed by serialising it: a mutex for multi-step invariants, atomics for one value, a WaitGroup to await completion.In the text: Data race and synchronisation
- Pass a cancel signal down so work can be stopped
- A value threaded down a call chain that signals cancellation to everything derived from it and carries the reason, with time.After giving a testable timeout.In the text: Context
Annotated passagesAI reading companion
Lines worth stopping on, each with a note on why it matters to the whole book.
“It is good to separate your "domain" code from the outside world \(side-effects”
Read in context →Read at book scale, this aside is the architectural thesis every later chapter re-derives: the seam between domain and side effects exists because of testability, not elegance.
“If your tests are causing you pain, listen to that signal and think about the design of your code.”
Read in context →The single sentence the whole book is an argument for — test pain is a design signal, which retroactively explains DI, mocking, context and the HTTP chapters as one idea.
“You're doing something else if you are "refactoring" some code and having to change tests at the same time.”
Read in context →Gives the book's most-used word a falsifiable definition, turning 'refactor' from a mood into a claim the test suite can adjudicate.
9 more notes
“just break it! (make sure you have committed your changes to source control first though)”
Read in context →The epistemic floor of the entire method: green means nothing until you have seen the test fail for the right reason — the move that separates TDD from test-after theater.
“By being disciplined it highlighted a flaw in our tests and this is a good thing!”
Read in context →Names the book's quiet reversal: tests are specification, not verification, which is why so many chapters find missing constraints rather than bugs.
“A mistake people often make with shiny new language features is they start by using them without having a concrete use-case.”
Read in context →The strongest discipline claim in the book — abstraction is only safe when extracted from a concrete case, which indicts premature generics, premature interfaces, and premature packages alike.
“Make the test work quickly, committing whatever sins necessary in process.”
Read in context →Licenses the 'commit sins' half of TDD that the whole application section runs on, and is exactly the half teams quote while skipping the non-negotiable refactor.
“What we need to do is to be able to inject \(which is just a fancy word for pass in\) the dependency of printing.”
Read in context →Deflates the book's most-feared enterprise concept into function arguments, and that deflation is what makes every later testable design look obvious in hindsight.
“So in the book "Learn Go with Tests" for the first time, we're not going to write a test.”
Read in context →A TDD book marking where TDD stops paying is the credibility purchase for all forty other chapters — the method is presented as judgment, not creed.
“Optimism is an occupational hazard of programming. Feedback is the treatment.”
Read in context →The cleanest statement of why the book's loop exists at all: TDD is treatment for a character flaw in programmers, not a quality process bolted onto code.
“These assumptions are usually not validated beyond manual checking, so they threaten your test suite's usefulness”
Read in context →Reframes the mocking chapters in retrospect — the hazard was never test doubles but unvalidated assumptions duplicated across a suite.
“Don't mistake this for TDD being hard, it's design that's hard!”
Read in context →Worth keeping as the book's self-sealing move: reassigning every TDD failure to design skill makes the method hard to falsify even where it is mostly right.
A reader’s verdictAI reading companion
Pretends to teach Go while stress testing a single design claim
Read end to end, this book is not really a Go tutorial and barely pretends to be one by the halfway mark. Its actual subject is a single claim, stated in the “Why unit tests and how to make them work for you” chapter and then stress-tested for forty chapters: software must change, change requires refactoring, refactoring requires tests, and therefore the quality of your tests is the quality of your design. Everything else — syntax, goroutines, templates, generics, WebSockets — is a series of excuses to apply pressure to that claim and see whether it holds. The spine runs in three movements. First, the primer chapters (5–24), where trivial problems are deliberately chosen so that the loop itself is the lesson: Hello World exists to make you watch failures and even sabotage t.
Helper() to see line numbers degrade; Integers lets return 4 stand as a legitimate objection rather than waving it away; Iteration smuggles in benchmarks as if measurement were ordinary; Pointers and Maps hand you a contradiction (two addresses, a nil-write panic) instead of a rule to memorise. Second, the design chapters (12–18, 25–28, 41), where the book stops teaching Go and starts arguing: Dependency Injection and Mocking reframe test pain as design feedback; Context stages a two-act betrayal in which a passing, faithfully TDD'd server is thrown away for being unidiomatic; the “Working without mocks, stubs and spies” chapter indicts the entire stub-and-spy tradition for scattering unvalidated assumptions, and answers with stateful fakes disciplined by contracts run against both fake and real; the “Scaling Acceptance Tests (and light intro to gRPC)” chapter splits specification from transport so convincingly that a whole new protocol lands without touching a test.
Continue reading
Third, the application chapters (29–40), where the method is made to survive a real codebase — a scoreboard server discovered rather than planned, persistence arriving as a sequence of concessions rather than a design, a CLI whose real subject is package boundaries, and a poker game whose dependency bloat is read as a signal that a Game domain type wants to exist. The sharpest judgements are the ones that cut against the reader's instincts. Untestable code is not code that needs a test flag; it is badly designed code (37, 40). A passing test suite is not evidence of good design (18, 35, where two habitual -race runs undo tests that looked finished).
Abstraction belongs in the refactor step, discovered from code you already wrote, not designed up front (21, 24) — and should be reverted without shame when it fails to earn its keep (16). Reflection is taught thoroughly and then withdrawn as a caution, with the author admitting the implementation still crashes on pointer cycles. Property-based tests are shown to surface truths examples missed, then immediately corrected: guarding inside the assertion hides the mismatch, you must narrow generation instead (19). Templates should stay logic-less, because logic inside them can only be tested by comparing rendered HTML (22).
Refactoring is defined by one mechanical test — did your tests have to change? — and the book then follows its own rule, refactoring production code first, then tests, never both at once (28, 34). The honesty is the pedagogy: acceptance tests get a candid ledger of drawbacks (25), the author confesses he didn't believe his own passing library, the "maths is relevant today!" pitch is never actually cashed, and the “Build an application” chapter's fictional product owner is admitted as a convenience. What I'd say to a reader: do not skim the small chapters and do not read this for Go. The toy problems are where the loop becomes muscle memory, and the later chapters are unreadable as mere tutorials — the “Working without mocks, stubs and spies” chapter only lands if you have felt the mocking chapter's unease, and the “Scaling Acceptance Tests (and light intro to gRPC)” chapter only lands if you have watched tests couple to implementation and rot.
Type the code, let the compiler talk, keep the steps smaller than feels dignified, and treat every awkward test as a message about your design rather than a chore to finish. The book's final gift, delivered in the “TDD Anti-patterns” chapter, is permission to stop numbing that pain: tests are not a coverage obligation, they are the cheapest honest feedback you will ever get about the thing you are actually building.
What reviewers say
Reviewers in the excerpts praise this free, open-source book for teaching Go interactively through tests: fast feedback, running examples instead of abstract words, coverage from types and pointers to context and concurrency, plus web and CLI applications. Several frame it as a TDD and software-design book written in Go, recommended for newcomers. One complaint: later chapters make snippet placement unclear.
A synthesis of published reviews. Ratings are not shown; follow the links for each review in full.
Sources and license
MITThe text
- Learn Go with Tests by Chris James (quii). Copyright (c) 2025 Christopher James. Released under the MIT License (https://github.com/quii/learn-go-with-tests/blob/main/LICENSE.md). Read it free · License
- Summaries, claims, notes and essays are AI-assisted: generated by FeedLitra from the full text, with quotations checked against the source. Last updated October 22, 2026.
Read Learn Go with Tests with these cards beside the text.
Open in FeedLitra