Registered users can read the full content for free
Register as a Gaohf Library member to read the complete e-book online for free and enjoy a better reading experience.
Page
1
(This page has no text content)
Page
2
Learn Go with Tests Chris James Creative Commons Non-Commercial Share Alike 3.0
Page
3
Learn Go with Tests Learn Go with Tests Why unit tests and how to make them work for you Hello, World Integers Iteration Arrays and slices Structs, methods & interfaces Pointers & errors Maps Dependency Injection Mocking Concurrency Select Reflection Sync Context Roman Numerals Mathematics Reading files HTML Templates Generics Revisiting arrays and slices with generics Build an application HTTP Server JSON, routing & embedding IO and sorting Command line and project structure Time WebSockets OS Exec Error types Context-aware readers HTTP Handlers Revisited
Page
4
TDD Anti-patterns
Page
5
Learn Go with Tests Art by Denise Support me I am proud to offer this resource for free, but if you wish to give some appreciation Tweet me @quii Buy me a coffee Sponsor me on GitHub
Page
6
Learn test-driven development with Go Explore the Go language by writing tests Get a grounding with TDD. Go is a good language for learning TDD because it is a simple language to learn and testing is built- in Be confident that you'll be able to start writing robust, well-tested systems in Go Translations: 中文 Português 日本語 한국어 Türkçe Background I have some experience introducing Go to development teams and have tried different approaches as to how to grow a team from some people curious about Go into highly effective writers of Go systems. What didn't work Read the book An approach we tried was to take the blue book and every week discuss the next chapter along with the exercises. I love this book but it requires a high level of commitment. The book is very detailed in explaining concepts, which is obviously great but it means that the progress is slow and steady - this is not for everyone.
Page
7
I found that whilst a small number of people would read chapter X and do the exercises, many people didn't. Solve some problems Katas are fun but they are usually limited in their scope for learning a language; you're unlikely to use goroutines to solve a kata. Another problem is when you have varying levels of enthusiasm. Some people just learn way more of the language than others and when demonstrating what they have done end up confusing people with features the others are not familiar with. This ends up making the learning feel quite unstructured and ad hoc. What did work By far the most effective way was by slowly introducing the fundamentals of the language by reading through go by example, exploring them with examples and discussing them as a group. This was a more interactive approach than "read chapter x for homework". Over time the team gained a solid foundation of the grammar of the language so we could then start to build systems. This to me seems analogous to practicing scales when trying to learn guitar. It doesn't matter how artistic you think you are, you are unlikely to write good music without understanding the fundamentals and practicing the mechanics. What works for me When I learn a new programming language I usually start by messing around in a REPL but eventually, I need more structure.
Page
8
What I like to do is explore concepts and then solidify the ideas with tests. Tests verify the code I write is correct and documents the feature I have learned. Taking my experience of learning with a group and my own personal way I am going to try and create something that hopefully proves useful to other teams. Learning the fundamentals by writing small tests so that you can then take your existing software design skills and ship some great systems. Who this is for People who are interested in picking up Go People who already know some Go, but want to explore testing more What you'll need A computer! Installed Go A text editor Some experience with programming. Understanding of concepts like if, variables, functions etc. Comfortable with using the terminal Feedback Add issues/submit PRs here or tweet me @quii MIT license
Page
9
Why unit tests and how to make them work for you Here's a link to a video of me chatting about this topic If you're not into videos, here's wordy version of it. Software The promise of software is that it can change. This is why it is called soft ware, it is malleable compared to hardware. A great engineering team should be an amazing asset to a company, writing systems that can evolve with a business to keep delivering value. So why are we so bad at it? How many projects do you hear about that outright fail? Or become "legacy" and have to be entirely re- written (and the re-writes often fail too!) How does a software system "fail" anyway? Can't it just be changed until it's correct? That's what we're promised! A lot of people are choosing Go to build systems because it has made a number of choices which one hopes will make it more legacy-proof. Compared to my previous life of Scala where I described how it has enough rope to hang yourself, Go has only 25 keywords and a lot of systems can be built from the standard library and a few other small libraries. The hope is that with Go you can write code and come back to it in 6 months time and it'll still make sense. The tooling in respect to testing, benchmarking, linting & shipping is first class compared to most alternatives.
Page
10
The standard library is brilliant. Very fast compilation speed for tight feedback loops The Go backward compatibility promise. It looks like Go will get generics and other features in the future but the designers have promised that even Go code you wrote 5 years ago will still build. I literally spent weeks upgrading a project from Scala 2.8 to 2.10. Even with all these great properties we can still make terrible systems, so we should look to the past and understand lessons in software engineering that apply no matter how shiny (or not) your language is. In 1974 a clever software engineer called Manny Lehman wrote Lehman's laws of software evolution. The laws describe a balance between forces driving new developments on one hand, and forces that slow down progress on the other hand. These forces seem like important things to understand if we have any hope of not being in an endless cycle of shipping systems that turn into legacy and then get re-written over and over again. The Law of Continuous Change Any software system used in the real-world must change or become less and less useful in the environment It feels obvious that a system has to change or it becomes less useful but how often is this ignored? Many teams are incentivised to deliver a project on a particular date and then moved on to the next project. If the software is "lucky" there is at least some kind of hand-off to another set of individuals to maintain it, but they didn't write it of course.
Page
11
People often concern themselves with trying to pick a framework which will help them "deliver quickly" but not focusing on the longevity of the system in terms of how it needs to evolve. Even if you're an incredible software engineer, you will still fall victim to not knowing the future needs of your system. As the business changes some of the brilliant code you wrote is now no longer relevant. Lehman was on a roll in the 70s because he gave us another law to chew on. The Law of Increasing Complexity As a system evolves, its complexity increases unless work is done to reduce it What he's saying here is we can't have software teams as blind feature factories, piling more and more features on to software in the hope it will survive in the long run. We have to keep managing the complexity of the system as the knowledge of our domain changes. Refactoring There are many facets of software engineering that keeps software malleable, such as: Developer empowerment Generally "good" code. Sensible separation of concerns, etc etc Communication skills Architecture Observability Deployability Automated tests
Page
12
Feedback loops I am going to focus on refactoring. It's a phrase that gets thrown around a lot "we need to refactor this" - said to a developer on their first day of programming without a second thought. Where does the phrase come from? How is refactoring just different from writing code? I know that I and many others have thought we were doing refactoring but we were mistaken Martin Fowler describes how people are getting it wrong However the term "refactoring" is often used when it's not appropriate. If somebody talks about a system being broken for a couple of days while they are refactoring, you can be pretty sure they are not refactoring. So what is it? Factorisation When learning maths at school you probably learned about factorisation. Here's a very simple example Calculate 1/2 + 1/4 To do this you factorise the denominators, turning the expression into 2/4 + 1/4 which you can then turn into 3/4. We can take some important lessons from this. When we factorise the expression we have not changed the meaning of the expression. Both of them equal 3/4 but we have made it easier for us to work with; by changing 1/2 to 2/4 it fits into our "domain" easier.
Page
13
When you refactor your code, you are trying to find ways of making your code easier to understand and "fit" into your current understanding of what the system needs to do. Crucially you should not be changing behaviour. An example in Go Here is a function which greets name in a particular language func Hello(name, language string) string { if language == "es" { return "Hola, " + name } if language == "fr" { return "Bonjour, " + name } // imagine dozens more languages return "Hello, " + name } Having dozens of if statements doesn't feel good and we have a duplication of concatenating a language specific greeting with , and the name. So I'll refactor the code. func Hello(name, language string) string { return fmt.Sprintf( "%s, %s", greeting(language), name, ) } var greetings = map[string]string { "es": "Hola", "fr": "Bonjour", //etc.. } func greeting(language string) string { greeting, exists := greetings[language]
Page
14
if exists { return greeting } return "Hello" } The nature of this refactor isn't actually important, what's important is I haven't changed behaviour. When refactoring you can do whatever you like, add interfaces, new types, functions, methods etc. The only rule is you don't change behaviour When refactoring code you must not be changing behaviour This is very important. If you are changing behaviour at the same time you are doing two things at once. As software engineers we learn to break systems up into different files/packages/functions/etc because we know trying to understand a big blob of stuff is hard. We don't want to have to be thinking about lots of things at once because that's when we make mistakes. I've witnessed so many refactoring endeavours fail because the developers are biting off more than they can chew. When I was doing factorisations in maths classes with pen and paper I would have to manually check that I hadn't changed the meaning of the expressions in my head. How do we know we aren't changing behaviour when refactoring when working with code, especially on a system that is non-trivial? 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.
Page
15
In order to safely refactor you need unit tests because they provide Confidence you can reshape code without worrying about changing behaviour Documentation for humans as to how the system should behave Much faster and more reliable feedback than manual testing An example in Go A unit test for our Hello function could look like this func TestHello(t *testing.T) { got := Hello(“Chris”, es) want := "Hola, Chris" if got != want { t.Errorf("got %q want %q", got, want) } } At the command line I can run go test and get immediate feedback as to whether my refactoring efforts have altered behaviour. In practice it's best to learn the magic button to run your tests within your editor/IDE. You want to get in to a state where you are doing Small refactor Run tests Repeat All within a very tight feedback loop so you don't go down rabbit holes and make mistakes. Having a project where all your key behaviours are unit tested and give you feedback well under a second is a very empowering safety net to do bold refactoring when you need to. This helps us manage the incoming force of complexity that Lehman describes.
Page
16
If unit tests are so great, why is there sometimes resistance to writing them? On the one hand you have people (like me) saying that unit tests are important for the long term health of your system because they ensure you can keep refactoring with confidence. On the other you have people describing experiences of unit tests actually hindering refactoring. Ask yourself, how often do you have to change your tests when refactoring? Over the years I have been on many projects with very good test coverage and yet the engineers are reluctant to refactor because of the perceived effort of changing tests. This is the opposite of what we are promised! Why is this happening? Imagine you were asked to develop a square and we thought the best way to accomplish that would be stick two triangles together. We write our unit tests around our square to make sure the sides are equal and then we write some tests around our triangles. We want to make sure our triangles render correctly so we assert that the angles sum up to 180 degrees, perhaps check we make 2 of them, etc etc. Test coverage is really important and writing these tests is pretty easy so why not?
Page
17
A few weeks later The Law of Continuous Change strikes our system and a new developer makes some changes. She now believes it would be better if squares were formed with 2 rectangles instead of 2 triangles. She tries to do this refactor and gets mixed signals from a number of failing tests. Has she actually broken important behaviours here? She now has to dig through these triangle tests and try and understand what's going on. It's not actually important that the square was formed out of triangles but our tests have falsely elevated the importance of our implementation details. Favour testing behaviour rather than implementation detail When I hear people complaining about unit tests it is often because the tests are at the wrong abstraction level. They're testing implementation details, overly spying on collaborators and mocking too much. I believe it stems from a misunderstanding of what unit tests are and chasing vanity metrics (test coverage). If I am saying just test behaviour, should we not just only write system/black-box tests? These kind of tests do have lots of value in terms of verifying key user journeys but they are typically expensive to write and slow to run. For that reason they're not too helpful for refactoring because the feedback loop is slow. In addition black box
Page
18
tests don't tend to help you very much with root causes compared to unit tests. So what is the right abstraction level? Writing effective unit tests is a design problem Forgetting about tests for a moment, it is desirable to have within your system self-contained, decoupled "units" centered around key concepts in your domain. I like to imagine these units as simple Lego bricks which have coherent APIs that I can combine with other bricks to make bigger systems. Underneath these APIs there could be dozens of things (types, functions et al) collaborating to make them work how they need to. For instance if you were writing a bank in Go, you might have an "account" package. It will present an API that does not leak implementation detail and is easy to integrate with. If you have these units that follow these properties you can write unit tests against their public APIs. By definition these tests can only be testing useful behaviour. Underneath these units I am free to refactor the implementation as much as I need to and the tests for the most part should not get in the way. Are these unit tests? YES. Unit tests are against "units" like I described. They were never about only being against a single class/function/whatever. Bringing these concepts together
Page
19
We've covered Refactoring Unit tests Unit design What we can start to see is that these facets of software design reinforce each other. Refactoring Gives us signals about our unit tests. If we have to do manual checks, we need more tests. If tests are wrongly failing then our tests are at the wrong abstraction level (or have no value and should be deleted). Helps us handle the complexities within and between our units. Unit tests Give a safety net to refactor. Verify and document the behaviour of our units. (Well designed) units Easy to write meaningful unit tests. Easy to refactor. Is there a process to help us arrive at a point where we can constantly refactor our code to manage complexity and keep our systems malleable? Why Test Driven Development (TDD) Some people might take Lehman's quotes about how software has to change and overthink elaborate designs, wasting lots of time
Page
20
upfront trying to create the "perfect" extensible system and end up getting it wrong and going nowhere. This is the bad old days of software where an analyst team would spend 6 months writing a requirements document and an architect team would spend another 6 months coming up with a design and a few years later the whole project fails. I say bad old days but this still happens! Agile teaches us that we need to work iteratively, starting small and evolving the software so that we get fast feedback on the design of our software and how it works with real users; TDD enforces this approach. TDD addresses the laws that Lehman talks about and other lessons hard learned through history by encouraging a methodology of constantly refactoring and delivering iteratively. Small steps Write a small test for a small amount of desired behaviour Check the test fails with a clear error (red) Write the minimal amount of code to make the test pass (green) Refactor Repeat As you become proficient, this way of working will become natural and fast. You'll come to expect this feedback loop to not take very long and feel uneasy if you're in a state where the system isn't "green" because it indicates you may be down a rabbit hole. You'll always be driving small & useful functionality comfortably backed by the feedback from your tests.
The above is a preview of the first 20 pages. Register to read the complete e-book.
AI Reading Assistant
Whole-book reading guide from stratified index samples; jump to passages in the text
AI guide
【One-Line Pitch】
A hands-on introduction to Go that teaches the language through test-driven development, using small, real projects to build both idiomatic Go skills and disciplined engineering habits. Best for developers who already program in another language and want to learn Go by doing, not by reading reference manuals.
【Book Arc】
- **Opening (~0%–10%)**: Establishes the core feedback loop—write a failing test, make it pass, then refactor—and applies it to the smallest possible programs: a Hello function, integer addition, and iteration. Introduces the idea of separating domain logic from side effects so code becomes testable.
- **Early (~10%–30%)**: Builds up Go's type system through TDD: structs, methods, interfaces (including Go's implicit interface satisfaction), pointers and value-copy semantics, maps, and error handling as values. Moves into concurrency with goroutines, channels, `select`, `sync.WaitGroup`, and `context`, plus benchmarking and the "make it work, make it right, make it fast" ordering.
- **Middle (~30%–50%)**: Applies TDD to richer problems—reflection-based walking of arbitrary data, a Roman numeral converter, trigonometry-driven SVG clock rendering, and file-system abstractions using `io/fs`, `embed`, and `html/template` for a blog renderer.
- **Late (~50%+ of excerpt coverage)**: Continues expanding the blog renderer into an index page and beyond, reinforcing API design through tests and dependency injection. The excerpts do not cover the final chapters in detail.
- **Ending**: Not covered by the excerpts; the guide cannot describe the book's closing material.
【Key Takeaways】
- **Refactoring means changing structure without changing behavior** (Opening): the book grounds this in the maths idea of factorisation—rewriting an expression so it fits your domain better while remaining equivalent. This framing makes refactoring a disciplined step, not a vague cleanup.
- **Separate domain logic from side effects to make code testable** (Opening): pulling `fmt.Println` out of a `Hello` function so the string-producing logic can be tested in isolation is the book's first and most repeated design lesson.
- **Go interfaces are satisfied implicitly** (Early): a type satisfies an interface simply by having the required methods—no `implements` declaration. This enables decoupling helpers from concrete types and is a recurring design tool throughout the book.
- **Method receivers are copies** (Early): the Wallet example shows that value receivers copy the value, so mutations don't persist. Understanding this is essential before pointers and stateful types make sense.
- **Errors are values** (Early): extracting a shared error variable gives a single source of truth and lets tests assert on meaning rather than exact wording—a pattern the book reuses for dictionary operations and beyond.
- **Concurrency is introduced through tests, not theory** (Early): goroutines, channels, `select`, `sync.WaitGroup`, and `context` cancellation are all exercised via failing tests and benchmarks, including a race between two async operations.
- **Benchmarking enforces the work/right/fast order** (Early): the `CheckWebsites` benchmark with a deliberately slow stub shows how to measure before optimizing, echoing Knuth's warning against premature optimization.
- **Thin vertical slices beat big designs** (Early–Middle): the Roman numeral and clock chapters repeatedly start with the smallest useful case (1, then 10, then 1984) rather than designing the whole solution upfront.
【Reading Tips】
- **Deep-read the Opening and Early chapters.** The TDD cycle, interface semantics, and pointer/value distinctions are foundational; skimming them will make later chapters confusing.
- **Type the code yourself.** This is a tutorial book; the value comes from running failing tests and watching them pass, not from reading finished solutions.
- **Skim the reflection and SVG/trigonometry chapters on first pass** if you're not immediately applying them—they're valuable but narrower in everyday use. Return when you need them.
- **Treat the blog renderer as a case study in API design.** Notice how each new feature starts with a test that forces a design decision, and how dependency injection and `io.Writer` keep code loosely coupled.
- **Don't test third-party libraries.** The book explicitly warns against it; use approval tests and dependency injection instead.
【Coverage Limits】
The excerpts cover roughly the first half of the book in detail, with the later chapters (including the ending) only partially represented. Specific chapter titles, final projects, and concluding material are not described here.
Passage locations
Excerpt 1
ot := Hello() want := "Hello, world" if got != want { t.Errorf("got %q want %q", got, want) } } Go modules? Goodness me, we have more requirements. We now ne...
View in text
Excerpt 2
r Update We could reuse ErrNotFound and not add a new error. However, it is often better to have a precise error for when an update fails. Having specific er...
View in text
Excerpt 3
in fact it could be captured in a class structure instead. Go isn't strictly OO but that doesn't mean we ignore the lessons OO offers entirely (as much as so...
View in text
Excerpt 4
impler than maintaining a folder of test files, and it will execute quicker. Write the test first Let's extend our test further to extract the next line from...
View in text
Recommended for You
{{#thumbnailUrl}}
{{/thumbnailUrl}}
{{^thumbnailUrl}}
{{/thumbnailUrl}}
Loading recommended books...
Failed to load, please try again later
Tip the Site
Scan the WeChat Pay or Alipay code to tip. No login required.
WeChat Pay
Alipay