The guide
How the course works, and how to think through a problem.
50 exercises in 10 tiers, about 108.5 h of work, and 11 certificates.
The story
The exercises follow 1 story. You are the new junior SRE at Vesper Labs, an AI lab on floors 58 to 64 of the Spire, in the city of Meridian. The lab sells HUSH, an AI that holds the pager for 40 000 companies, so that their engineers can sleep.
Vesper Labs has 1 rule, etched in the lobby glass: no AI writes the code that watches the AI. In this course it is rule 1. You write every tool by hand, and each one goes into the Ledger with you as its author.
Who you work with
- Adaeze NwosuCEO. 64-01, corner office
- Daniyar SeitkaliCTO. 63-04, the lab
- Noor HaddadPrincipal SRE. Your mentor. 61-12, next to your desk
- Wren LindqvistSRE. Your rotation partner. 61-14, by the swap board
- Marisol Quispe MamaniSupport lead. 59-02, support desk
- HUSHThe model. Rows A to K, 40 m down
- FuseCat. The warmest rack
Why there is a story
A tool is easier to care about when you know who reads its output at 03:12. So each exercise opens with a page from the night: a ticket, a log line, a dashboard that lies. The story exists to make Go practice fun.
What stays exact
The story lives in the first paragraph of each exercise and in its data: log lines, tickets, host names. The task, the rules and the tests are exact and plain. The hints and the explanations teach without the story, and the story does not change what an exercise teaches.
On this site, Story effects in the header turn off the story's screens, such as the Calm Score. The exercises stay the same.
Why this exists
An LLM can write the code for each of these tasks in seconds. If it always does, a set of skills fades: breaking a vague task into steps, choosing a data structure, reading the docs until you understand a function, and debugging from a failing test to the cause. These exercises train exactly those skills. The tasks are real: you meet each of them in production work, and none of them is a puzzle with a trick. The difficulty comes from the problem itself.
Tiers 1 to 5 assume that you can program and did A Tour of Go once. Their explanations also cover Go basics where a bug hides in them, such as map order, slices that share memory and pointer receivers. Tiers 6 to 10 assume that you write Go fluently. Their explanations do not explain loops, structs or maps. All explanations cover the Go semantics that cause bugs, the design choices, and how production systems solve the same problem.
The rules
The goal is to train the part of your brain that solves problems. Prompting is a different skill.
- Do not use an LLM. That includes chat assistants and AI autocomplete (Copilot, Cursor Tab, Codeium). Turn them off in your editor.
goplsis fine. - Use documentation freely.
go doc, pkg.go.dev, the Go spec, Effective Go, the Go blog, man pages and RFCs are all allowed. Finding the answer in the docs is part of the skill. - Try for at least 25 minutes before you reveal hint 1. Wait at least 10 minutes between hints.
- If you read the reference solution, close it, wait, and write your own version from memory. Do not copy it.
- Read the explanation and the walkthrough after you pass, too. They show the trade-offs and how production systems do it.
How to work an exercise
- Read the spec twice. It is on the left of the exercise page.
- Read the tests: open them from the Test files menu above the editor. Find the case names and the helper functions. They tell you what "done" means.
- Work 2 small examples by hand, on paper.
- Write down the state your code must keep and what must always be true about it: the invariants.
- Write the simplest version that can pass 1 test.
- Run 1 step at a time: open the Steps tab and press Run step 1. The site runs with
-raceunless you clear the box. - Make it correct. Then make it clean.
- Keep
-raceon. An exercise passes only when all its tests pass with the race detector. - Read the explanation and the walkthrough: levels 4 and 5 of the hint ladder. Compare them with your solution.
- Keep a log of where you got stuck: the Notes tab of the exercise. The course page keeps the time, the runs and the hints you used.
The checklist
Ask these questions before you write code. They replace "ask the LLM".
- What are the inputs and the outputs, exactly? What are their types?
- What does the smallest interesting example look like? What is the answer by hand?
- What state must I keep between steps? What changes it?
- What must always be true about that state?
- What are the edges? Empty input, 1 element, duplicates, huge input, malformed input, a cancelled context, a clock that moves backwards, 2 goroutines at the same time.
- Which known shape is this? A parser (state machine), an aggregation (map, then sort), a scheduler (heap), dependencies (graph), a lifecycle (state machine), a fan-out (goroutines,
sync.WaitGroup,context). - Can I split it into pure functions plus a thin layer that does the effects (time, I/O, goroutines)? The pure part is easy to test.
- How do I know I am done? The tests, plus the edge cases from my own list.
If you are stuck:
- Make the failing case smaller until you understand it.
- Print the state at each step with
t.Logforfmt.Printf("%+v\n", x). - Explain the problem out loud, step by step. This is what you used the LLM for. Do it yourself.
- Read the docs of the function you call:
go doc bufio.Scanner.Buffer. - Walk away for 10 minutes.
How to read Go messages
The editor explains every problem that the compiler, go vet, staticcheck or a test reports. When no explanation fits a message, it links 1 of these 4 guides. Read them once before your first exercise.
How to read a Go error message
The parts of a message
The Go tools print every problem in the same shape:
./retry.go:12:5: undefined: backof
./retry.gois the file, relative to the directory where the tool ran.12is the line and5the column. The column counts bytes from 1, so a tab counts as 1.- Everything after the column is the message: what the tool found, often with the name it is about. The message can contain colons too, as in
undefined: backof.
A line such as # example.com/retry above the messages names the package that failed. Some type errors add indented lines below the message, such as have (int) and want (string). They belong to the message above them.
The first error matters most
Fix the errors from the top down, and run the tools again after each fix. 1 mistake, such as a missing brace or a wrong type, can cause more errors further down that disappear once the first one is fixed. The compiler stops after 10 errors with too many errors, so a long list is not complete anyway.
Who reports it
- The compiler: the code does not compile. Nothing runs until you fix it.
go vet: the code compiles, but a construct is almost always a bug.go testruns some vet checks first and stops when they fail.- staticcheck: the code compiles, but a value is lost or a call cannot do what you expect. The code at the end, such as
SA4006, is the name of the check: look it up on staticcheck.dev.
In the editor
Each problem is a marker at its line and an entry in the Problems list. Read the message first, then look at the exact column it points to, and only then at the code around it.
Learn more
How to read a stack trace
The first line
A crash starts with what went wrong: panic: and the value that was passed to panic, or fatal error: for a problem the program cannot recover from, such as a deadlock. In a test you may see [recovered] after it: the testing package caught the panic to report the failing test, then panicked again.
Goroutines and frames
Then Go prints the goroutines, each with a header such as goroutine 19 [running]:. The word in brackets is the state: running, or what the goroutine waits for, such as chan receive, sync.Mutex.Lock or sync.WaitGroup.Wait.
Below each header come the frames, the most recent call first. Every frame has 2 lines:
example.com/retry.Do({0x5b1e20, 0xc000012345}, 0x3)
/work/retry.go:42 +0x1d
The first line is the function with its package path. The arguments are raw machine words in hex, often marked with ? because they can be stale, and (...) means the call was inlined. Do not try to read your values from them. The second line is the file and the line that function was executing. The +0x1d offset is a position in the machine code; you can ignore it. A line created by ... in goroutine 7 shows where this goroutine was started.
Find your frame
Read down from the top and skip the frames in runtime and testing. The first frame in the exercise package is where it happened. The frame below it shows who called it, often a line in a test file. Open both lines.
For a deadlock or a test timeout, Go prints every goroutine. Look for the goroutines in the exercise package and for what each one waits on. 2 goroutines that wait for each other, or a receive that no send will ever match, is the usual cause.
Learn more
How to read a race report
What a report says
With -race, Go watches every memory access while the tests run. When 2 goroutines touch the same memory, at least 1 of them writes, and nothing orders the 2 accesses, it prints a report between 2 lines of = signs. The test that was running fails with race detected during execution of test.
The 4 blocks
WARNING: DATA RACE
Write at 0x00c000012345 by goroutine 8:
...frames...
Previous read at 0x00c000012345 by goroutine 7:
...frames...
Goroutine 8 (running) created at:
...frames...
Goroutine 7 (finished) created at:
...frames...
- The access that was caught: a read or a write, the address, and the goroutine.
- The earlier access to the same address by another goroutine.
- and 4. Where each of the 2 goroutines was started.
In every block, skip the frames of runtime, sync and testing. The first frame in the exercise package is the line that matters. The same address in both blocks means the same variable, often a field of a struct, a map or a slice element.
What to fix
Name the shared variable, then decide who owns it. Protect it with a sync.Mutex that every access takes, pass it through a channel so only 1 goroutine owns it at a time, or give each goroutine its own copy and combine the results after wg.Wait(). sync/atomic fits a single counter or flag. A time.Sleep does not fix a race: it only makes it rarer.
The detector finds only the races that happen during this run. Run the tests with -race often, and more than once.
Learn more
How to read a failing test
The name
--- FAIL: TestSplit/two_fields (0.00s) names the test that failed. After the slash comes the subtest, the case of a table-driven test; spaces in its name become underscores. The time in parentheses is how long it ran.
The line
Under the name, each message starts with a file and a line, such as split_test.go:42:. That is where the test called t.Errorf or t.Fatalf. Open that line: the code just above it shows the input and how the test checks the result. t.Errorf lets the test go on and report more problems; t.Fatalf stops this test.
Got and want
The course tests write their messages as the call, what your code returned, and what the test expected:
Split("a,b") = [a,b], want [a b]
Compare the 2 values character by character. A difference that is hard to see, such as a space, a quote or a missing element, is often the bug. For an Example, the output under got: is what your code printed and want: is the // Output: comment of the example. Only leading and trailing space of the whole output is ignored.
Run 1 test
Run only the failing step while you fix it: open the Steps tab and press its Run button. The Output tab shows everything the tests printed, also the logs of the tests that pass.
If the output says panic: test timed out, a test ran longer than the timeout allows. Usually it waits for something that never happens. The goroutine list below it shows where it waits.
Learn more
Keys in the editor
- Ctrl+Enter
- Run the tests of the step you picked.
- Ctrl+S
- Save now. The editor also saves 1.5 seconds after you stop typing.
- F8, Shift+F8
- Go to the next or the previous problem.
- Shift+Alt+F
- Format the file with gofmt.
- Ctrl+M, then Tab
- Tab moves the focus out of the editor instead of typing a tab. Press Ctrl+M again to type tabs.
On a Mac, use Cmd in place of Ctrl.
Make it a tool
The exercises are libraries, because libraries are easy to test. An SRE ships small tools. After you pass an exercise, you can wrap it in a command. These follow-ups have no tests:
| Exercise | Tool |
|---|---|
| 02-repeated | A collapse filter on stdin that folds repeated lines. |
| 11-nightlog | nightlog -gap 10m < node.log: a summary of the night and its silences. |
| 12-linefilter | A -filter flag for any log command. |
| 13-nines | nines -objective 99.95 outages.csv: the budget and the real downtime. |
| 18-tableprint | tableprint: aligned columns from JSON lines. |
| 25-syslog | syslog2logfmt: each syslog line as a logfmt line, the input of 26-logfmt. |
| 26-logfmt | A filter on stdin: logfilter -level warn -match db=orders. |
| 27-accesslog | An incident report from an access log file. |
| 29-grep | Your own grep -C with a regular expression. |
| 38-logmerge | A stern-like merge of several log files. |
| 42-promtext | A promtool check metrics-like linter, plus rate between 2 scrapes. |
| 43-certcheck | A cron job that exits with 0 (OK), 1 (warning) or 2 (critical). |
| 50-loadbalancer | A runnable load balancer with flags for the backends. |
Write main as a thin shell around a function you can test:
func main() { os.Exit(run(os.Args[1:], os.Stdin, os.Stdout, os.Stderr)) }
func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
// Parse flags with flag.NewFlagSet, do the work, return the exit code.
return 0
}
A test calls run with a strings.Reader and 2 bytes.Buffer values. It needs no process and no real files.