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
| 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.▸ |
No exercises match — clear the search or change filters.