Skip to content

Catalog

All 172 exercises in one place — filter by check mode or topic, or search. Click any row for the run command and the hint; expand a topic header for what it teaches.

  • 172Exercises
  • 51Topics
  • 26🔨 Compile
  • 146🧪 Test
Two check modes: 🔨 Compile — the exercise passes once it builds (and the linter is happy). 🧪 Test — a test file drives it; make the failing tests pass. Every exercise ships broken on purpose — remove the I AM NOT DONE marker once it's fixed. ▶ Play — solve it in the browser. Most exercises open on the Go Playground; the multi-file capstone stages open on goplay.tools, where the browser (WebAssembly) runtime runs their tests.
Mode Topic Exercise Description
Variables Beginner · Fundamentals 6 exercises
▤ Chapter Background A variable binds a name to a piece of memory of a fixed type. In Go both halves of that sentence are enforced at compile time: the name must exist before you assign to it, and the type is fixed the moment the variable is declared and can never change afterwards. A lot of Go's beginner errors are the compiler holding you to those two rules. The syntax is small — `var` with a type, `var` with a value, `:=` inside a function, `const` for compile-time values — and these six exercises walk through each form by breaking it. Every failure here is a *compile* error, which is the point: the whole class of "variable does not exist yet" and "wrong type" bugs never reaches a running program.▸
Compile Variables variables1 ▶Play Give the variable a name so it can be declared.▸
Compile Variables variables2 ▶Play Declare a variable before assigning to it.▸
Compile Variables variables3 ▶Play A variable declaration needs a type or an initial value.▸
Compile Variables variables4 ▶Play See what happens when a block reuses a name with a new type.▸
Compile Variables variables5 ▶Play Constants must be given a value when declared.▸
Compile Variables variables6 ▶Play Constants cannot be reassigned after declaration.▸
Primitive Types Beginner · Fundamentals 5 exercises
▤ Chapter Background Go's built-in types are deliberately boring: booleans, sized integers, two floats, two complex types, and strings. What is not boring is the rule around them — **Go never converts numeric types for you**. Adding an `int` to an `int64` does not compile. That looks pedantic next to C or Python until the first time it stops a silent truncation in code handling money or byte offsets. These five exercises are all compile errors: a type that does not exist, a variable used before it is declared, a byte literal with nothing in it. The lesson underneath them is that in Go the type is part of the variable, chosen once, and the compiler will not quietly bend it.▸
Test Primitive Types primitive_types1 ▶Play Toggle a boolean before the second check. A bool can be reassigned a new value; here the store opens, then closes, so each check fires exactly once.▸
Compile Primitive Types primitive_types2 ▶Play Declare the string before formatting it.▸
Compile Primitive Types primitive_types3 ▶Play Declare the variables passed to Printf.▸
Compile Primitive Types primitive_types4 ▶Play Store a character in a byte value.▸
Compile Primitive Types primitive_types5 ▶Play Use Go's real numeric type names.▸
If Beginner · Fundamentals 2 exercises
▤ Chapter Background `if` is the plainest thing in Go, and it is worth one chapter anyway — because its *shape* is what most Go code looks like. There is no ternary operator, no pattern matching, no truthiness. What there is instead is a statement form that scopes a variable to the branch that needs it, and a convention (the early return) that keeps the happy path flat down the left-hand side of the function. Both exercises here are small: return the larger of two numbers, and map three inputs to three outputs. The interesting part is what the idiomatic solution looks like once the compiler's rules are taken seriously.▸
Test If if1 ▶Play Return the larger of two numbers using if.▸
Test If if2 ▶Play Map each input string to an output with if/else.▸
Switch Beginner · Fundamentals 3 exercises
▤ Chapter Background `switch` is the multi-branch conditional, and Go's version is more useful than the C one it looks like. It switches on any comparable value, not just integers; it can drop the value entirely and switch on conditions; each case can list several values; and — the big one — **cases do not fall through**, so the forgotten `break` that has bitten every C programmer is not a bug you can write here. The three exercises cover the two forms. `switch1` switches on a variable, `switch2` uses the conditionless form, and `switch3` maps seven integers to seven strings — the shape where `switch` clearly beats an `if/else` ladder.▸
Compile Switch switch1 ▶Play Switch on a variable instead of bare values.▸
Compile Switch switch2 ▶Play A conditionless switch needs a boolean case or default.▸
Test Switch switch3 ▶Play Return a weekday name for a number using a switch.▸
Functions Beginner · Fundamentals 4 exercises
▤ Chapter Background A function in Go declares its name, its parameters *with their types*, and what it returns. All three are checked at compile time and none of them can be inferred from the call site, which is why all four exercises here fail to compile rather than misbehave at run time — a missing type, a missing argument, a missing return type. The bigger idea underneath the syntax is that **functions are values**. A function has a type (`func(int) string`), can be stored in a variable, passed to another function, returned from one, and — with a receiver — attached to a type as a method. Everything from `sort.Slice` to `http.HandlerFunc` to the range-over-func iterators of Go 1.23 rests on that.▸
Compile Functions functions1 ▶Play Define the function that main calls.▸
Compile Functions functions2 ▶Play Function parameters must declare their types.▸
Compile Functions functions3 ▶Play Call a function with the argument it expects.▸
Compile Functions functions4 ▶Play A function returning a value must declare its return type.▸
More Functions Beginner · Fundamentals 2 exercises
▤ Chapter Background Two techniques that come up constantly once the basics are in place: a function that calls itself, and a function that accepts any number of arguments. Both are small additions to the syntax from the previous chapter, and both have one sharp edge worth knowing before you use them — an unbounded stack in the first case, a shared backing array in the second.▸
Test More Functions morefn1 ▶Play morefn1 — recursion A function may call itself. Every recursion needs a base case to stop. Implement factorial recursively.▸
Test More Functions morefn2 ▶Play morefn2 — variadic functions A variadic parameter (nums ...int) accepts any number of arguments and is seen inside the function as a slice. A slice can be spread with slice... .▸
Strings Beginner · Fundamentals 2 exercises
▤ Chapter Background A Go string is an **immutable sequence of bytes**. Not characters — bytes. It is conventionally UTF-8 encoded, and everything confusing about strings in Go follows from those two facts: `len("héllo")` is 6, `s[0]` gives you a number, and you cannot assign to `s[0]` at all. Once the byte/character distinction is clear, the rest is a small standard library: `strings` for whole-string operations, `unicode/utf8` for counting and decoding, `[]rune` when you truly need per-character indexing, and `strings.Builder` when you are assembling one in a loop.▸
Test Strings strings1 ▶Play The strings package holds the everyday string utilities.▸
Test Strings strings2 ▶Play A Go string is bytes, not characters. len(s) counts BYTES; one multibyte UTF-8 character (like é or 世) is several bytes. Count runes instead.▸
Arrays Beginner · Collections & Loops 2 exercises
▤ Chapter Background An array in Go is a fixed-length sequence whose **length is part of its type**. `[3]int` and `[4]int` are two different types: a function taking one will not accept the other, and there is no growing, appending, or resizing. Arrays are also **values** — assigning one or passing it to a function copies every element. That combination makes arrays rare in everyday Go. Slices, the next chapter, are what you actually use. Arrays are worth understanding anyway, because every slice is a window onto one, and because the value semantics that make arrays awkward are exactly what make them useful in the few places they belong.▸
Test Arrays arrays1 ▶Play Arrays are zero-indexed — fix the out-of-range access. The first element is at index 0 and the last of a length-3 array is at index 2.▸
Compile Arrays arrays2 ▶Play An array can't mix types; every element shares one type.▸
Slices Beginner · Collections & Loops 4 exercises
▤ Chapter Background A slice is the workhorse collection in Go, and it is three words of data: a **pointer** to an array, a **length**, and a **capacity**. That is the whole type. Everything surprising about slices — why `append` sometimes affects a second slice and sometimes does not, why passing one to a function lets it mutate your data but not extend it — falls straight out of those three fields. The four exercises here build the picture from the bottom: create one, take a sub-slice, append to it, and index it out of bounds.▸
Compile Slices slices1 ▶Play Create a slice with make, giving its element type.▸
Test Slices slices2 ▶Play Take a sub-slice using [low:high] bounds. A sub-slice includes low up to but NOT including high, so the last two of a length-4 array are [2:4].▸
Test Slices slices3 ▶Play Add elements to a slice with append. append returns a new slice with the extra elements added to the end.▸
Test Slices slices4 ▶Play Index a slice within its bounds.▸
Maps Beginner · Collections & Loops 3 exercises
▤ Chapter Background A map is Go's hash table: `map[K]V` associates keys of one type with values of another, with average O(1) lookup, insert, and delete. Two of its behaviours catch newcomers out, and both are deliberate — reading a missing key returns the **zero value** rather than an error, and iteration order is **deliberately randomised**. These three exercises cover declaring one, filling one from a literal, and the missing-key case that quietly returns `""`.▸
Compile Maps maps1 ▶Play Declare a map's key and value types.▸
Test Maps maps2 ▶Play Initialize a map with a literal. A map literal builds and fills a map in one expression: map[K]V{k: v, ...}.▸
Test Maps maps3 ▶Play Look up the correct key in a map.▸
Range Beginner · Collections & Loops 3 exercises
▤ Chapter Background Go has exactly one loop keyword, `for`, and `range` is the clause that walks a collection with it. One form covers slices, arrays, strings, maps, channels, integers (Go 1.22), and functions (Go 1.23) — with the pair of values it yields depending on what you hand it. The subtlety worth carrying away is that `range` gives you a **copy** of each element, and evaluates its subject **once**, up front. Both are why the loop sometimes does not do what a first reading suggests.▸
Compile Range range1 ▶Play Iterate a slice with range.▸
Compile Range range2 ▶Play Iterate a map's keys and values with range.▸
Test Range range3 ▶Play Use range to collect the even numbers.▸
Structs Intermediate · Types & Methods 3 exercises
▤ Chapter Background A struct is a fixed set of named fields laid out in one block of memory. That is the whole idea, and it is the main way Go models data — there are no classes, no inheritance, and no constructors. A type is its fields; behaviour is attached separately with methods; and reuse comes from **embedding** one struct in another rather than from a hierarchy. These three exercises build a `Person`: declare its fields, embed a second struct into it, and attach a method.▸
Test Structs structs1 ▶Play Define the struct's fields. A struct groups named fields, each with its own type.▸
Test Structs structs2 ▶Play Embed a struct instead of adding a plain field. Embedding promotes the inner struct's fields onto the outer one, so they can be read directly — person.phone rather than person.contact.phone.▸
Test Structs structs3 ▶Play Attach a method to a struct. A method returning a formatted string is easy to get subtly wrong — a missing space between the names still compiles. The test pins the exact result.▸
Pointers Intermediate · Types & Methods 3 exercises
▤ Chapter Background Go passes **everything** by value. A function receives a copy of each argument, so writing to a parameter changes nothing the caller can see. A pointer is how you opt out: instead of the value, you pass its address, and the function writes through that address to the caller's own memory. That is the entire purpose, and these three exercises are the entire story — write through a `*int`, mutate a struct through a `*Account`, and see side by side that a value parameter cannot do what a pointer parameter can.▸
Test Pointers pointers1 ▶Play A pointer holds the address of a value. &x takes the address; *p reads or writes the value at that address. Modify the caller's int through a pointer.▸
Test Pointers pointers2 ▶Play Passing a *struct lets a function mutate the caller's struct. Go auto-dereferences, so you write a.Field even when a is a pointer.▸
Test Pointers pointers3 ▶Play A value parameter receives a COPY, so changes don't reach the caller. Only a pointer parameter can mutate the caller's data. Make incPointer work (incValue is intentionally a no-op from the caller's view).▸
Methods Intermediate · Types & Methods 2 exercises
▤ Chapter Background A method is a function with a **receiver** — an extra parameter written before the name, binding the function to a type. That is all the syntax there is, and it buys three things: dot-call notation, interface satisfaction, and a natural home for behaviour next to the data it operates on. The one decision that matters is the receiver's form. `(r T)` receives a copy; `(r *T)` receives an address. That choice is not just about mutation — it decides which values can satisfy an interface, which is where it surprises people.▸
Test Methods methods1 ▶Play Methods attach behavior to a type. A value receiver (r Rectangle) works on a copy and can only read; a pointer receiver (r *Rectangle) can mutate the original. Implement both.▸
Test Methods methods2 ▶Play You can define methods on ANY named type you declare, not just structs.▸
Interfaces Intermediate · Types & Methods 4 exercises
▤ Chapter Background An interface is a list of method signatures. Any type that has those methods satisfies it — **implicitly**, with no `implements` keyword and no declared relationship. That inversion is the defining feature of Go's type system: the *consumer* declares what it needs, and types written before that interface existed can satisfy it. The four exercises walk the whole surface: satisfy an interface with two types, implement the standard-library `Stringer`, recover the concrete type out of an interface value, and compose interfaces by embedding.▸
Test Interfaces interfaces1 ▶Play An interface is a set of method signatures. A type satisfies it IMPLICITLY, just by having those methods — there is no "implements" keyword. Give Rectangle and Circle an Area method so both satisfy Shape.▸
Test Interfaces interfaces2 ▶Play Implementing fmt.Stringer (String() string) controls how a value prints. The `var _ fmt.Stringer = p` line is a compile-time check that Point satisfies the interface.▸
Test Interfaces interfaces3 ▶Play Recover the concrete type held by an interface value with a type switch (or a comma-ok type assertion).▸
Test Interfaces interfaces4 ▶Play Interfaces can embed other interfaces. A type satisfies the combined interface by implementing every embedded method. Implement *Buffer.▸
Enums Intermediate · Types & Methods 2 exercises
▤ Chapter Background Go has no `enum` keyword. What it has instead is a defined integer type plus a `const` block using `iota`, and the combination gets you most of what an enum provides: a distinct type the compiler will not confuse with a plain `int`, a set of named values, and — with a `String()` method — readable output. What it does **not** get you is exhaustiveness. Nothing stops `Weekday(42)`, and nothing warns when a `switch` misses a case. Knowing where the guarantee stops is the point of this chapter.▸
Test Enums enums1 ▶Play Go has no enum keyword; the idiom is a named integer type plus a const block using iota, which yields 0, 1, 2, ... for successive constants.▸
Test Enums enums2 ▶Play Pair iota constants with a String() method to get readable names (and free pretty-printing via fmt, since this satisfies fmt.Stringer).▸
Type Aliases Intermediate · Types & Methods 3 exercises
▤ Chapter Background Two declarations that look almost identical do completely different things: ```go type Celsius float64 // DEFINED TYPE — a brand-new type type MyByte = byte // ALIAS — a second name for an existing type ``` The `=` is the whole difference. A defined type shares its underlying representation but is a **distinct type** the compiler will not mix with anything else. An alias is not a type at all — it is a name, resolved away, and completely interchangeable with what it names. The three exercises make each half concrete: convert across a defined type, hang a method on one, and see an alias pass with no conversion at all.▸
Compile Type Aliases typealias1 ▶Play A defined type like `type Celsius float64` is DISTINCT from float64 — Go will not mix the two without an explicit conversion, which stops accidental unit mix-ups at compile time.▸
Test Type Aliases typealias2 ▶Play Methods can be attached to a defined type. Give Celsius a String() method and the fmt package will use it automatically (the fmt.Stringer interface).▸
Test Type Aliases typealias3 ▶Play A type ALIAS (`type MyByte = byte`, note the = sign) is not a new type — it is a second name for the SAME type. Values are interchangeable with no conversion, unlike a defined type.▸
Anonymous Functions Intermediate · Functions & Errors 3 exercises
▤ Chapter Background A function literal is a function written without a name, inline, where it is needed. It has a type like any other function (`func(string)`), so it can be called immediately, stored in a variable, passed as an argument, or returned. What makes it more than shorthand is what happens when it mentions a variable from the enclosing scope: the literal becomes a **closure**, and that variable outlives the function it was declared in. That single mechanism is behind `defer`, callbacks, middleware, iterators, and every "give me a function that remembers something" pattern in Go.▸
Compile Anonymous Functions anonymous_functions1 ▶Play Pass an argument to an anonymous function.▸
Compile Anonymous Functions anonymous_functions2 ▶Play Match a closure's signature and use its argument.▸
Compile Anonymous Functions anonymous_functions3 ▶Play Return a closure that remembers state across calls.▸
Defer Intermediate · Functions & Errors 2 exercises
▤ Chapter Background `defer` schedules a call to run when the surrounding **function** returns — not when the block ends, and not at some point later. It runs on every exit path: a normal return, an early return, a `t.Fatal`, even a panic on its way up the stack. That last property is what makes it the cleanup mechanism in Go. There is no `finally`, no RAII, no destructors; there is `defer`, sitting one line below the thing it cleans up, where a reader can see the pair together.▸
Test Defer defer1 ▶Play Deferred calls run when the surrounding function returns, in LIFO order (last deferred runs first). Using a named return value, the defers can even shape what is returned.▸
Test Defer defer2 ▶Play defer is the idiomatic way to guarantee cleanup runs on every return path (the classic `f, _ := os.Open(...); defer f.Close()` pattern).▸
Errors Intermediate · Functions & Errors 6 exercises
▤ Chapter Background Go has no exceptions. A function that can fail returns an `error` as its last result, and the caller decides what to do with it on the very next line. That is the whole model, and the `if err != nil` blocks it produces are the most recognisable thing about the language. The type behind it is tiny: ```go type error interface { Error() string } ``` Anything with an `Error() string` method is an error. Everything else — the sentinels, the wrapping, `Is`, `As`, `Join` — is convention and standard-library helpers built on that one method. These six exercises cover the whole toolkit.▸
Test Errors errors1 ▶Play In Go, errors are ordinary values returned alongside results. Make divide return an error when the divisor is zero.▸
Test Errors errors2 ▶Play Wrapping an error with %w keeps the original reachable via errors.Is. Make lookup wrap the ErrNotFound sentinel.▸
Test Errors errors3 ▶Play A custom error type is any type implementing the error interface. errors.As extracts a wrapped error of a concrete type. Implement ValidationError.Error so the message is built from its field.▸
Test Errors errors4 ▶Play panic/recover is Go's last-resort mechanism, not normal error handling. Use a deferred function to recover from a panic and turn it into an error.▸
Test Errors errors5 ▶Play errors.Join (Go 1.20) bundles several errors into one. errors.Is then matches ANY of the joined errors — handy for collecting validation failures.▸
Test Errors errors6 ▶Play errors.Is walks the ENTIRE wrap chain: fmt.Errorf("...: %w", err) links a new message onto an existing error, and errors.Is finds a sentinel no matter how many layers deep it sits. Using %v instead of %w flattens the error to plain text and severs the chain.▸
Generics Intermediate · Generics & Modern Go 4 exercises
▤ Chapter Background Before Go 1.18, writing one function that worked for both `[]int` and `[]string` meant either copy-pasting it per type or routing everything through `interface{}` and giving up type checking at the door. Generics remove that trade: a function or type can take **type parameters**, the compiler checks each use against a **constraint**, and the code stays as statically typed as anything you write by hand. The four exercises build the whole vocabulary — a type parameter, a constraint that admits several types, a generic data structure, and a generic function that takes a function.▸
Compile Generics generics1 ▶Play Write one function that works for any type.▸
Test Generics generics2 ▶Play Constrain a generic to add numbers of any numeric type. A type constraint lists the types a parameter may take; a generic function then works for every one of them while keeping the same body.▸
Test Generics generics3 ▶Play Build a generic data structure: a Stack[T] that works for any element type.▸
Test Generics generics4 ▶Play Write a generic Reduce that folds any slice into a single accumulated value.▸
Modern Intermediate · Generics & Modern Go 5 exercises
▤ Chapter Background Go changes slowly, but the releases since 1.21 changed enough of the everyday vocabulary that code written from memory five years ago now reads as dated. This chapter is the shortlist: the built-ins you no longer write helpers for, the two loop changes, and the concurrency helper that removes a boilerplate people got wrong. Everything here is language-level or standard-library — nothing to import, nothing to install.▸
Test Modern modern1 ▶Play Go 1.21 added built-in min and max (any ordered type, two or more args) and clear (empties a map or zeroes a slice). No import needed.▸
Test Modern modern2 ▶Play Go 1.22: a for-range loop can iterate over an integer directly — `for i := range n` runs with i = 0, 1, ..., n-1.▸
Test Modern modern3 ▶Play Go 1.23: for-range can iterate over an iterator function. An iter.Seq[V] is func(yield func(V) bool) — call yield for each value; stop if it returns false. Implement the iterator's body.▸
Test Modern modern4 ▶Play Go 1.25: WaitGroup.Go(f) launches f in its own goroutine and takes care of Add(1) and Done() for you. It replaces the classic — and easy to get wrong — wg.Add(1); go func() { defer wg.Done(); ... }() dance.▸
Test Modern modern5 ▶Play Go 1.22 changed for-loop semantics: each iteration declares NEW loop variables instead of reusing one across the whole loop. Closures and pointers created inside the loop therefore capture a value of their own — the classic "all my goroutines saw the last item" bug is gone, but only when the variable really is declared by the loop.▸
Iterators Intermediate · Generics & Modern Go 4 exercises
▤ Chapter Background Before Go 1.23, producing a sequence lazily meant one of three unsatisfying options: build the whole slice up front, hand out a channel (a goroutine, a close, and a leak if the consumer left early), or invent a `Next()`/`Err()` interface per package. Range-over-function replaces all of it. An iterator is just a **function of a particular shape**, and `for range` knows how to drive it: ```go type Seq[V any] func(yield func(V) bool) type Seq2[K, V any] func(yield func(K, V) bool) ``` No goroutine, no channel, no allocation per element, and `break` works.▸
Test Iterators iter1 ▶Play iter.Seq[V] is func(yield func(V) bool) — the shape of a range-over-func iterator. A Filter wraps a source sequence and re-yields only the values that match a predicate.▸
Test Iterators iter2 ▶Play iter.Seq2[K, V] is func(yield func(K, V) bool) — an iterator over pairs, like ranging a map or a slice's index/value. Enumerate pairs each element with its index, so the FIRST value yielded is the index and the SECOND is the element.▸
Test Iterators iter3 ▶Play slices.Collect(seq) drains an iter.Seq[V] into a []V. It does NOT accept an iter.Seq2[K, V]; to collect the values of a pair-iterator you first project it down to a single-value iterator. valuesOf is that adapter.▸
Test Iterators iter4 ▶Play A `range` loop drives a push iterator from start to finish — you cannot pause one, walk another, and resume. iter.Pull flips a push iterator (iter.Seq) into a next/stop pair you call yourself, which is what lockstep work over two sequences needs.▸
Modules Intermediate · Packages & Modules 11 exercises
▤ Chapter Background Every chapter so far has quietly lived inside a module: the `go.mod` at the root of this repo. This chapter makes that machinery visible. A **package** is a directory of `.go` files compiled together; a **module** is a tree of packages versioned and released as one unit, described by the `go.mod` at its root. The exercises run offline (`GOPROXY=off`), so every dependency here is a local directory. The parts of the module system that need a network — downloading, checksums, version selection across a registry — are covered in prose at the end.▸
Test Modules modules1 ⚡Play Every module starts with a go.mod, and its first job is to name the module. That name — the module path — is the import prefix for every package inside it. The tests import this package as example.com/hello; declare the module so that path resolves.▸
Test Modules modules2 ⚡Play An import path is the module path from go.mod followed by the package's directory inside the module. Checkout needs the cart package in ./cart — fix the import so the toolchain can find it.▸
Test Modules modules3 ⚡Play Only names that start with an upper-case letter are visible outside their package. The tests are in a separate package, temp_test, so they see exactly what any other importer would. Export what they use.▸
Test Modules modules4 ⚡Play A package inside an internal/ directory can only be imported by code rooted at internal's parent. money lives in ledger/internal/, so only ledger may use it. Format the balance through ledger's public API instead.▸
Test Modules modules5 ⚡Play The app imports example.com/mathx, a separate module that sits next to it in ./mathx with its own go.mod. It is not published anywhere, so go.mod has to do two things: require the module, and say where on disk to find it.▸
Test Modules modules6 ⚡Play From v2 on, a module's major version is part of its path: go.mod already requires example.com/units/v2. The import path has to say so too — the package name inside stays plain units.▸
Test Modules modules7 ⚡Play The go line is not a comment about which toolchain you happen to have: it sets the language version the module is compiled as. Code in this module uses a feature newer than the version declared here.▸
Test Modules modules8 ⚡Play The email channel registers itself in its init function — but a package's init only runs if the package is part of the program. Make sure it is, without calling anything from it.▸
Test Modules modules9 app depends on example.com/greet v1.0.0, but you are changing greet at the same time and want app to build against your local copy in ./greet. A go.work file lists the modules developed together; add greet to it.▸
Test Modules modules10 go.mod has drifted from the code. It still requires oldfmt, which nothing imports any more, and it never recorded pad, which table needs. Don't fix it by hand: from this directory, let the toolchain rewrite go.mod to match the imports. The test checks the result.▸
Test Modules modules11 This module vendors its dependencies: vendor/ holds a copy of every module it needs, and the build reads from there instead of the module cache. Someone added the slug dependency to go.mod without refreshing vendor/, and the build now refuses to run. Bring vendor/ back in line with go.mod.▸
Dependency Injection Intermediate · Testable Design 3 exercises
▤ Chapter Background Dependency injection sounds like a framework and is actually a habit: a function or type receives its collaborators instead of reaching out for them. In Go it needs no container, no annotations, and no code generation — a parameter and an interface are the whole mechanism. The reason to care is not architectural purity. It is that code which calls `os.Stdout`, `time.Now()`, or a database handle directly cannot be tested without providing a real one. Inject those and the same code runs against a `bytes.Buffer`, a fixed clock, and an in-memory store.▸
Test Dependency Injection di1 ▶Play Inject an io.Writer so the output can be captured and asserted in a test.▸
Test Dependency Injection di2 ▶Play Inject behaviour through a small interface to make time-dependent code testable.▸
Test Dependency Injection di3 ▶Play Constructor injection: a struct holds a dependency interface and delegates to it.▸
Mocking Intermediate · Testable Design 2 exercises
▤ Chapter Background Once a dependency arrives through an interface, a test can pass something else in its place. In Go that something else is usually **ten lines of ordinary code in the test file** — no mocking library, no code generation, no expectation DSL. The two shapes cover almost everything: a **spy** records how it was called so you can assert on the interaction, and a **stub** (or fake) returns canned results so you can drive the logic, including the error paths that are otherwise hard to reach.▸
Test Mocking mock1 ▶Play A spy is a test double that records how it was called, so you can assert on it.▸
Test Mocking mock2 ▶Play A stub/fake returns canned data so you can test logic, including error paths.▸
Concurrent Advanced · Concurrency 3 exercises
▤ Chapter Background A goroutine is a function that runs while the rest of your program keeps going. You start one by putting `go` in front of a call, and that is the whole syntax — there is no thread object to configure, no pool to size, no future to hold. What you give up is control: the caller does not know when the goroutine runs, whether it has finished, or what it saw when it read a variable you were writing. Everything else in this series exists to buy that control back. `sync.WaitGroup` answers *are they done*, channels answer *how do I hand a value over*, mutexes answer *who may touch this memory*, and `context` answers *how do I tell them to stop*. These three exercises cover the first two, and deliberately walk into the data race that motivates the rest.▸
Test Concurrent concurrent1 ▶Play Print from goroutines and wait with a WaitGroup.▸
Test Concurrent concurrent2 ▶Play Safely increment a counter from many goroutines.▸
Test Concurrent concurrent3 ▶Play Send values over a channel and receive them.▸
Channels Advanced · Concurrency 2 exercises
▤ Chapter Background A channel is a typed pipe with a lock and a queue inside it. `chan int` carries ints and nothing else; one goroutine puts values in with `ch <- v`, another takes them out with `<-ch`, and the runtime makes that exchange safe without you writing a mutex. The arrow always points the way the value moves. The part worth internalising is that a channel is not only a container — it is a **synchronization point**. A send on an unbuffered channel does not finish until a receiver takes the value, so the two goroutines meet at that line. Buffering loosens the meeting; direction (`chan<- T`, `<-chan T`) documents who is allowed to do which half. These two exercises drill both.▸
Test Channels channels1 ▶Play A buffered channel — make(chan T, n) — holds up to n values without a receiver ready. An unbuffered channel blocks the sender until someone receives, which would deadlock the single-goroutine loop below.▸
Test Channels channels2 ▶Play Channel direction in a signature documents and enforces intent: chan<- T is send-only, <-chan T is receive-only.▸
Select Advanced · Concurrency 3 exercises
▤ Chapter Background A channel operation on its own commits you to one channel: `<-ch` waits for `ch` and nothing else. `select` removes that commitment. It lists several channel operations, blocks until **one** of them can proceed, runs that case, and abandons the rest. That one primitive covers most of what real concurrent code needs: take whichever answer arrives first, give up after a deadline, poll without blocking, and stop when someone cancels. The three exercises here are those shapes; `context` and `concurrency_patterns` are them combined.▸
Test Select select1 ▶Play select waits on multiple channel operations and proceeds with whichever one is ready. Return the first value available from either channel.▸
Test Select select2 ▶Play Pairing a receive with time.After in a select implements a timeout.▸
Test Select select3 ▶Play A `default` case makes a select non-blocking: it runs when no other case is ready instead of waiting.▸
Sync Advanced · Concurrency 3 exercises
▤ Chapter Background "Share memory by communicating" is the advice, and it is good advice — but some state genuinely is shared. A counter, a cache, a connection pool, a config struct read by every request: passing those over a channel means routing every access through one owner goroutine, which is often more machinery than the problem deserves. For those cases the `sync` and `sync/atomic` packages let several goroutines touch the same memory safely. The three tools here cover the whole range. A `sync.Mutex` says *one goroutine at a time*. A `sync.Once` says *this runs exactly once, everyone else waits for it*. An atomic says *this single word updates indivisibly, no locking at all*. Pick by the shape of the state, not by taste.▸
Test Sync sync1 ▶Play A sync.Mutex guards shared state so concurrent goroutines don't race. Run with `go test -race` to see the unsynchronized version flagged.▸
Test Sync sync2 ▶Play sync.Once runs a function exactly once, even when called from many goroutines.▸
Test Sync sync3 ▶Play sync/atomic provides lock-free atomic operations on integers — cheaper than a mutex for a simple counter. Run with `go test -race` to catch the bug.▸
Context Advanced · Concurrency 3 exercises
▤ Chapter Background `select2` gave up waiting after a timeout, but the work on the other side of the channel carried on: the query still ran, the connection stayed open, the goroutine still finished into a result nobody read. Abandoning a result is not cancellation. `context.Context` is how a cancellation signal travels *into* the work. It is an immutable value you pass as the first argument of every call that might block, carrying three things: a `Done()` channel that closes when the caller gives up, an `Err()` saying why, and an optional deadline. Everything below the caller — your own goroutines, `database/sql`, `net/http`, gRPC — watches that one channel, so one `cancel()` unwinds the whole tree.▸
Test Context context1 ▶Play A context.Context lets a caller signal cancellation to running work. Make countUntilCancelled return once the context is cancelled.▸
Test Context context2 ▶Play context.WithTimeout cancels itself after a duration. Make doWork abort early and return ctx.Err() when the context is done.▸
Test Context context3 ▶Play Contexts can carry request-scoped values down a call chain. Read a value out of the context, falling back to a default when absent.▸
Concurrency Patterns Advanced · Concurrency 3 exercises
▤ Chapter Background Goroutines, channels, `select` and `context` are four small parts, and nearly all concurrent Go is three arrangements of them: spread work across N workers (**worker pool**), collapse N streams into one (**fan-in**), and chain stages that each transform a stream (**pipeline**). They share one skeleton — a goroutine that ranges over an input channel, sends to an output channel, and closes what it owns — so the interesting part is never the mechanics. It is the two questions every arrangement has to answer: *who closes the output*, and *what happens when the consumer stops early*.▸
Test Concurrency Patterns concpat1 ▶Play concpat1 — worker pool A fixed set of worker goroutines pulls jobs from one channel and pushes results to another. Implement the worker so the pool produces every result.▸
Test Concurrency Patterns concpat2 ▶Play concpat2 — fan-in Fan-in merges values from several input channels into one output channel. Implement merge so it copies every input into out and closes out when done.▸
Test Concurrency Patterns concpat3 ▶Play concpat3 — pipeline A pipeline chains stages connected by channels: each stage reads from the previous stage and emits to the next. Implement the `double` stage.▸
Goroutine Safety Advanced · Concurrency 3 exercises
▤ Chapter Background By this point in the series you can start goroutines, hand values between them, wait on several at once, guard shared state, and cancel it all. What is left is the discipline: the rules that decide *who* may touch a piece of state and *who* is allowed to close a channel. Those rules matter because the failure modes are so uneven. An unguarded integer gives a quietly wrong number. An unguarded map gives a **fatal error that no `recover` can catch**. Two goroutines closing the same channel gives a panic in whichever loses the race — often only under load, in production. This chapter is three concrete ownership decisions, each drilled under `go test -race`.▸
Test Goroutine Safety safety1 ▶Play A value shared across goroutines needs synchronization. sync.RWMutex allows many concurrent readers (RLock) but an exclusive writer (Lock) — a good fit for read-heavy state. With no lock at all, concurrent access is a data race.▸
Test Goroutine Safety safety2 ▶Play sync.Map is a concurrent map built for stable keys and many reads. Its Store/Load methods are safe for simultaneous use — a plain map is not, and concurrent writes to one crash with "fatal error: concurrent map writes".▸
Test Goroutine Safety safety3 ▶Play Channel ownership: the goroutine that OWNS a channel closes it — exactly once. Consumers must never close a channel they only receive from. Several workers each closing the same channel panics with "close of closed channel".▸
Synctest Advanced · Concurrency 2 exercises
▤ Chapter Background Concurrent code is testable right up to the moment time gets involved. Then the tests grow `time.Sleep(50 * time.Millisecond)` — long enough that the goroutine "should" be done, short enough that the suite still finishes. Both halves of that sentence are guesses. On a loaded CI box the sleep is too short and the test flakes; on your laptop the suite spends its life asleep. Testing a one-hour retry backoff this way is simply not possible. `testing/synctest` (experimental in Go 1.24, GA in **Go 1.25**) removes the guess. It runs a test inside a *bubble* with a **fake clock**: when every goroutine in the bubble is blocked, there is nothing left to wait for, so the clock jumps straight to the next timer. A one-hour sleep returns instantly, and "instantly" is exact rather than probable.▸
Test Synctest synctest1 ▶Play testing/synctest (Go 1.25) runs test code in a "bubble". synctest.Wait() blocks until every goroutine in the bubble is idle — a deterministic replacement for time.Sleep-and-hope. But Wait only works inside a bubble; call it outside one and it panics. The body must be wrapped in synctest.Test.▸
Test Synctest synctest2 ▶Play Inside a synctest bubble time is virtual: when EVERY goroutine (including the test's own) is blocked, the fake clock jumps to the next timer. So a one-hour time.Sleep finishes instantly — but only once the test goroutine actually blocks waiting for it. Read the clock without waiting and no time passes.▸
Stdlib Essentials Advanced · Standard Library & I/O 7 exercises
▤ Chapter Background Go's standard library is the reason so much production Go has a short `go.mod`. JSON, HTTP, time, regular expressions, and file I/O all ship with the toolchain, are covered by the compatibility promise, and are usually good enough that reaching for a dependency needs a reason. These seven exercises are a tour of the packages you will use in almost every program: `encoding/json`, `io`, `slices`, `time`, `strconv`, `regexp`, and one recent JSON addition worth knowing about.▸
Test Stdlib Essentials stdlib1 ▶Play stdlib1 — encoding/json Struct field tags control the JSON key names used during (un)marshaling. Add tags so the JSON keys map onto the struct fields.▸
Test Stdlib Essentials stdlib2 ▶Play stdlib2 — io.Reader / io.Writer These two interfaces are the backbone of Go I/O. io.Copy streams bytes from any Reader to any Writer. Drain the reader into the buffer.▸
Test Stdlib Essentials stdlib3 ▶Play stdlib3 — slices The slices package (Go 1.21+) provides generic helpers over slices. Sort the numbers in DESCENDING order.▸
Test Stdlib Essentials stdlib4 ▶Play stdlib4 — time Go formats and parses time using a REFERENCE date, not strftime codes: Mon Jan 2 15:04:05 MST 2006 (i.e. 01/02 03:04:05PM '06 -0700) Parse a date string using the layout "2006-01-02".▸
Test Stdlib Essentials stdlib5 ▶Play stdlib5 — strconv strconv converts between strings and numbers (and other base types).▸
Test Stdlib Essentials stdlib6 ▶Play stdlib6 — regexp regexp matches and extracts text using regular expressions.▸
Test Stdlib Essentials stdlib7 ▶Play stdlib7 — encoding/json omitzero `omitempty` predates most of Go's type system: it drops empty strings, zero numbers, false, and nil/empty collections — but a struct is never "empty" to it, so a zero time.Time marshals anyway. Go 1.24 added `omitzero`, which asks the type itself whether it is the zero value.▸
Maps Package Advanced · Standard Library & I/O 2 exercises
▤ Chapter Background The `maps` package (Go 1.21, extended in 1.23) is the map counterpart to `slices`: generic helpers for whole-map operations that everyone used to hand-roll — cloning, comparing, bulk deletion, and iterating keys and values. Note the name collision. This chapter is about the **standard-library package** `maps`; the `maps` *topic* earlier in the curriculum was about the built-in `map[K]V` type. They are different things with the same word.▸
Test Maps Package mapspkg1 ▶Play The maps package (Go 1.21+) has helpers for whole-map operations. maps.Clone returns an independent shallow copy — unlike assigning a map value, where both names refer to the SAME underlying map and a write through one is seen by the other.▸
Test Maps Package mapspkg2 ▶Play maps.DeleteFunc(m, pred) (Go 1.21) removes every entry for which pred(k, v) is true, in a single pass — no collect-the-keys-then-delete two-step needed.▸
Structured Logging Advanced · Standard Library & I/O 3 exercises
▤ Chapter Background `log/slog` (Go 1.21) replaced a decade of "which logging library?" with an answer in the standard library. A log record carries a message, a level, a time, and typed **key/value attributes** — so a log line is queryable data rather than a sentence you later regret writing a regex for. The split that matters: a **`Logger`** is the API you call, a **`Handler`** decides what happens to each record. Format, level filtering, destination and enrichment all live in the handler, which is why swapping JSON for text — or production for an in-memory test capture — changes one line.▸
Test Structured Logging slog1 ▶Play log/slog (Go 1.21) is structured logging in the stdlib. A handler has a minimum level and drops records below it. Here the handler is pinned to LevelWarn, so Info messages vanish.▸
Test Structured Logging slog2 ▶Play logger.With(attrs...) returns a NEW logger that carries those attributes on every record it writes. It does not modify the receiver — you have to use the logger it returns.▸
Test Structured Logging slog3 ▶Play A slog.Handler decides what happens to each record. Implementing one lets you capture logs in memory for tests. Every method is provided except Handle, which must record the message.▸
Reflection Advanced · Standard Library & I/O 2 exercises
▤ Chapter Background Reflection is the ability of a program to inspect and manipulate values whose types it does not know at compile time. In Go it lives in one package, `reflect`, and it powers the things that *cannot* be written any other way: `encoding/json`, database mappers, `fmt`'s `%v`, validators, and deep-equality in tests. It is also the part of Go where the compiler stops helping you. Every mistake becomes a run-time panic, the code is slower, and the tooling cannot see what you are doing. Most programs should reach for an interface or a type parameter first — and then, occasionally, this.▸
Test Reflection reflect1 ▶Play Inspect a value's kind at runtime with the reflect package.▸
Test Reflection reflect2 ▶Play Walk a struct's fields with reflection, calling a function for each string.▸
Unsafe Pkg Advanced · Standard Library & I/O 2 exercises
▤ Chapter Background `unsafe` is Go's escape hatch from the type system. It has no run-time representation and generates no code — it is a compile-time instruction to stop checking. With it you can read a struct's memory layout, reinterpret one type as another, and convert between `[]byte` and `string` without copying. The name is the documentation. Code using `unsafe` is exempt from the Go 1 compatibility promise, invisible to the race detector's usual guarantees, and capable of corrupting memory in ways the rest of Go cannot. These two exercises stick to the two well-established, spec-blessed idioms.▸
Test Unsafe Pkg unsafe1 ▶Play unsafe.Offsetof(x.Field) reports the byte offset of Field within its struct — layout introspection with zero runtime cost and no reflection.▸
Test Unsafe Pkg unsafe2 ▶Play unsafe.String(ptr, len) builds a string that SHARES the backing array of a []byte — a zero-copy conversion for hot paths, unlike string(b) which allocates and copies. The bytes must not be mutated afterwards.▸
Files Advanced · Standard Library & I/O 2 exercises
▤ Chapter Background File I/O in Go is two decisions: read the whole thing into memory, or stream it. The standard library makes both a one-liner, and picking wrong is how a service that worked on a 2 KB fixture falls over on a 2 GB log. Everything here sits on the `io.Reader`/`io.Writer` interfaces from the `stdlib_essentials` chapter, which is why the same code works against a file, a network connection, or a `strings.Reader` in a test.▸
Test Files files1 ▶Play files1 — reading and writing files os.WriteFile and os.ReadFile handle whole-file I/O in a single call. (The test uses t.TempDir(), a temp directory cleaned up automatically.)▸
Test Files files2 ▶Play files2 — reading line by line with bufio.Scanner bufio.Scanner reads any io.Reader one line at a time, without loading the whole input into memory — the usual way to process files or stdin.▸
Http Client Advanced · Standard Library & I/O 1 exercise
▤ Chapter Background `net/http` is a production HTTP client in the standard library — connection pooling, keep-alives, HTTP/2, redirects, and proxy support included. One exercise covers the shape; this chapter covers what a service needs beyond it.▸
Test Http Client http1 ▶Play http1 — HTTP client net/http makes requests. http.Get performs a GET; always close the response body. The test spins up a local httptest.Server so the call is deterministic.▸
Http Server Advanced · Building Applications 4 exercises
▤ Chapter Background Go is one of the few languages where the standard library is a complete answer for HTTP services. `net/http` gives you a production server, a router with method and path patterns (since Go 1.22), and a testing package that exercises handlers without a network. Frameworks exist; most services do not need one. The four exercises are the whole spine of a service: a handler, routing, JSON in and out, and middleware.▸
Test Http Server httpsrv1 ▶Play Write an http.HandlerFunc and test it with httptest, no real network needed.▸
Test Http Server httpsrv2 ▶Play Route requests with http.ServeMux using Go 1.22 method + path patterns.▸
Test Http Server httpsrv3 ▶Play Decode a JSON request body and encode a JSON response with encoding/json.▸
Test Http Server httpsrv4 ▶Play Wrap a handler with middleware (a func(http.Handler) http.Handler).▸
Http Server Advanced Advanced · Building Applications 3 exercises
▤ Chapter Background Three things a real service needs that the basics chapter left out: protection against cross-site requests, exact handling of path wildcards, and proxying to a backend. All three are in `net/http` and its `httputil` subpackage — still no framework.▸
Test Http Server Advanced httpadv1 ▶Play net/http's CrossOriginProtection (Go 1.25) blocks cross-site, state-changing requests using the browser's Sec-Fetch-Site metadata — CSRF defense with no tokens or cookies. You opt in by wrapping your handler with it.▸
Test Http Server Advanced httpadv2 ▶Play Go 1.22's ServeMux supports method + wildcard patterns like "GET /users/{id}/posts/{postID}". r.PathValue("postID") returns that path segment — but ONLY if the name matches a {wildcard} in the pattern exactly.▸
Test Http Server Advanced httpadv3 ▶Play httputil.ReverseProxy.Rewrite is the modern hook (replacing the deprecated Director field) for retargeting a request. Inside it, pr.SetURL(target) points the outbound request at the backend while preserving the inbound path.▸
Cli Advanced · Building Applications 2 exercises
▤ Chapter Background Go's `flag` package parses command-line flags, and for most tools it is all you need — no Cobra, no urfave/cli. What makes it worth a chapter is a structural point that generalises well beyond flags: using a **`FlagSet`** instead of the package-level functions turns argument parsing from untestable global state into an ordinary function you can call with a slice.▸
Test Cli cli1 ▶Play Parse command-line flags with the flag package.▸
Test Cli cli2 ▶Play Read the positional arguments left after flag parsing with FlagSet.Args.▸
Testing Advanced Advanced · Testing & Applied 4 exercises
▤ Chapter Background Every exercise in this repo has been run by `go test`, so the basics are already familiar: a `TestXxx(*testing.T)` in a `_test.go` file, `t.Errorf` to fail and continue, `t.Fatalf` to fail and stop. This chapter is the rest of the toolkit — the four things that separate a test suite from a pile of tests.▸
Test Testing Advanced testadv1 ▶Play testadv1 — table-driven tests with t.Run Table-driven tests pair a slice of cases with t.Run subtests, so each case reports independently. Implement fizzbuzz to make the table pass.▸
Test Testing Advanced testadv2 ▶Play testadv2 — fuzzing A fuzz test (func FuzzXxx(*testing.F)) feeds generated inputs to find edge cases; its seed corpus also runs during a normal `go test`. This reverse is buggy: it reverses BYTES, corrupting multibyte UTF-8. Fix it to reverse RUNES so the seed "Hello, 世界" stays valid.▸
Test Testing Advanced testadv3 ▶Play testadv3 — httptest net/http/httptest exercises HTTP handlers without a real network: build a request, record the response, assert on it. Implement the handler.▸
Test Testing Advanced testadv4 ▶Play testadv4 — benchmarks A benchmark (func BenchmarkXxx(*testing.B)) measures performance; run it with `go test -bench=.`. The Go 1.24+ form is `for b.Loop() { ... }`. The benchmark below is complete — your job is to implement sumSlice so the correctness test passes (normal `go test` runs the Test, not the Benchmark).▸
Profiling Advanced · Testing & Applied 3 exercises
▤ Chapter Background A benchmark tells you *that* something is slow. A profile tells you **where**. Go ships the whole toolchain: sampling profilers in the runtime, a `go tool pprof` viewer, and an HTTP endpoint that exposes both from a live process. The rule that comes before any of it: **measure, do not guess**. The bottleneck is almost never where it feels like it should be, and an optimisation applied without a profile is a change of unknown sign.▸
Test Profiling pprof1 ▶Play runtime/pprof writes a CPU profile you later inspect with `go tool pprof`. StartCPUProfile(w) begins sampling; the profile is only flushed to w when you call StopCPUProfile. Forget it and the output stays empty.▸
Test Profiling pprof2 ▶Play runtime.ReadMemStats fills a MemStats snapshot of the heap. To measure how much memory a piece of code retains, read stats before and after — calling runtime.GC() first so previously-freed garbage is not counted.▸
Test Profiling pprof3 ▶Play Importing net/http/pprof for its side effects registers the /debug/pprof/ handlers on http.DefaultServeMux. A server that uses its OWN mux won't expose them unless it forwards that path prefix to the default mux.▸
Applied Advanced · Testing & Applied 2 exercises
▤ Chapter Background Two exercises that stop drilling one feature and instead combine several, the way real code does. Implementing an interface the standard library defines, and building a small concurrency-safe type out of a map, a mutex, methods, and a sentinel error.▸
Test Applied applied1 ▶Play applied1 — sort.Interface Implementing sort.Interface (Len, Less, Swap) lets sort.Sort order ANY type. Len is provided; add Less and Swap.▸
Test Applied applied2 ▶Play applied2 — a concurrency-safe store Integrate several ideas at once: a map for storage, a sync.Mutex for safety, methods for the API, and a sentinel error for the missing-key case. Implement Set.▸
Capstone Capstone · Log-Ingest Service 8 exercises
▤ Chapter Background Every other topic in this curriculum drills one idea in isolation. The capstone does the opposite: one program, built across eight stages, where each stage adds a layer a real service actually needs and inherits every decision made before it. The service accepts log events over HTTP, validates them, batches them through a worker pool, stores them safely under concurrent access, logs structurally, shuts down without dropping work, parses an untrusted text format, and exposes profiling endpoints. These are also the only **package-mode** exercises in the repo: each stage spans several files in a directory, golings compiles the whole directory, and the earlier stages arrive already solved — so you can start at any stage, and reset one without losing the rest.▸
Test Capstone logingest1 ⚡Play The capstone builds one program across eight stages: a concurrent HTTP log-ingest service. Stage one is the data model — the Event that every later stage validates, stores, serves, batches, logs and profiles. This is the first exercise that spans more than one file. The implementation lives here, the tests live next door, and golings compiles the whole directory rather than a single file. Edit only what the FIXME points at.▸
Test Capstone logingest2 ⚡Play Stage two is storage. The service will accept events from many connections at once, so the thing holding them has to survive concurrent use: one goroutine appending while another reads must not corrupt the map, and a reader must not be handed a slice the store keeps mutating.▸
Test Capstone logingest3 ⚡Play Stage three puts the store behind HTTP. Go 1.22 gave ServeMux method and wildcard patterns, so the routing table is the mux itself rather than a hand-rolled switch on r.Method and a strings.Split of the path.▸
Test Capstone logingest4 ⚡Play Stage four puts a worker pool between the handler and the store. Ingest should not block on storage, and a burst of connections should not turn into a burst of goroutines — a fixed set of workers draining one channel gives you backpressure instead.▸
Test Capstone logingest5 ⚡Play Stage five is shutdown. A service that dies the instant it is signalled drops the requests it is in the middle of answering; a service that never dies has to be killed. Graceful shutdown is the middle: stop accepting new work, let the work in flight finish, then give up after a deadline.▸
Test Capstone logingest6 ⚡Play Stage six is observability. A log line built with fmt.Sprintf is fine to read and miserable to query; log/slog emits key/value pairs that a log aggregator can filter on. The request logger is middleware, so it wraps any handler without the handler knowing.▸
Test Capstone logingest7 ⚡Play Stage seven is the text ingest path: agents that cannot speak JSON send pipe-separated lines instead. Parsing untrusted text is where panics live, so this is also where fuzzing earns its keep — the test file below asserts an invariant and lets the fuzzer go looking for input that breaks it.▸
Test Capstone logingest8 ⚡Play Stage eight is the last one: expose the runtime profiles, then assemble the whole service. net/http/pprof is written for the default mux — its init() registers the handlers on http.DefaultServeMux and nothing else — so putting it on a mux you own takes one deliberate step.▸
Net Tcp Systems · The byox On-Ramp 4 exercises
▤ Chapter Background `net/http` hides a socket. This chapter opens one. A TCP server in Go is four calls — `Listen`, `Accept`, `Read`, `Write` — and everything else is a decision about *framing* (where does one message end?) and *concurrency* (who waits for whom?). Those two decisions are the whole difference between a toy and a server, and they are the same whether the protocol on top is Redis, HTTP, Kafka or one you invent this afternoon.▸
Test Net Tcp tcp1 ▶Play Accept one TCP connection and answer it.▸
Test Net Tcp tcp2 ▶Play Frame a byte stream into messages.▸
Test Net Tcp tcp3 ▶Play Run an accept loop that stops cleanly when the listener closes.▸
Test Net Tcp tcp4 ▶Play Serve connections concurrently, one goroutine each.▸
Binary Encoding Systems · The byox On-Ramp 4 exercises
▤ Chapter Background Text formats tell you what they contain; binary formats do not. A wire header is a fixed run of bytes at known offsets, and everything you get back depends on agreeing with the sender about **width**, **order** and **position**. Get one of them wrong and you do not get an error — you get a plausible wrong number. This chapter is `encoding/binary`, bit twiddling, and the two `io` interfaces that make it safe: `io.ReadFull` for streams and `io.ReaderAt` for files.▸
Test Binary Encoding binary1 ▶Play Decode a fixed-size big-endian header.▸
Test Binary Encoding binary2 ▶Play Pack and unpack a bitfield.▸
Test Binary Encoding binary3 ▶Play Read length-prefixed frames off a stream.▸
Test Binary Encoding binary4 ▶Play Read a fixed-size page at an offset with io.ReaderAt.▸
Process Control Systems · The byox On-Ramp 4 exercises
▤ Chapter Background A shell is a program that runs other programs, and `os/exec` plus `os/signal` is all the machinery that takes. The Go part is small; the ideas are Unix ones — a child inherits your environment and file descriptors, reports one byte of exit status, and can be interrupted by a signal it may or may not be allowed to catch. Three questions run through the chapter: what did the child *print*, what did it *read*, and how did it *end*.▸
Test Process Control process1 ▶Play Run a child process and capture what it printed.▸
Test Process Control process2 ▶Play Feed a child process on stdin and read what it writes back.▸
Test Process Control process3 ▶Play Tell a failed command apart from a command that never ran.▸
Test Process Control process4 ▶Play Catch a termination signal instead of dying on it.▸

▶ Run it

From the repo root: — or mise run watch to work through everything in order with instant feedback.