# Go From Zero

> Learn Go from nothing to genuinely advanced: install it and the basics, then the deep half - interfaces, generics, real concurrency patterns, error handling, the runtime scheduler and GC, testing and profiling, the standard library, and performance - all mental-model-first, with clear explanations.


---

# Go From Zero

You keep hearing that Go is the language behind Docker, Kubernetes, and half the cloud - that it's fast,
simple, and great at doing a thousand things at once. Then you open a Go file and it looks oddly bare:
no classes, no exceptions, a loop that doesn't say `while`, and a compiler that flat-out *refuses* to
build your program over an unused variable. That bareness isn't an accident, and it isn't something to
fight. Go was deliberately kept small so that a whole team can read each other's code without surprises.

This guide takes you the whole way: from "I've never written a line of Go" to understanding what the
language and its runtime are *actually doing* underneath your code. We'll go mental-model-first the whole
way: before any command, you'll understand what the thing actually *is* and why Go made the choice it did.

It's one zero-to-hero journey in two halves. **Phases 1–9 are the basics** - enough to read real Go,
structure a project, and use the concurrency it's famous for. **Phases 10–17 are the deep half** -
interfaces, generics, real concurrency patterns, the runtime scheduler and garbage collector, testing
and profiling, the standard library, and performance, the stuff that separates "writes Go" from
"understands Go." Each phase carries a difficulty badge so you can see the climb.

If you've never programmed at all, you'll want a gentler on-ramp first - start with
[Programming From Zero](/guides/programming-from-zero), then come back here.

## How to read this

- **Brand new to Go? Read 1–9 in order.** Each phase builds on the last. Phases 1–5 give you the language
  and how to organize it; phase 6 is the payoff (concurrency); 7–9 round you out into someone who can
  ship. Come back for 10+ when the basics feel comfortable.
- **Already know another language?** Skim phases 1–5 to catch where Go is *deliberately different* (one
  loop, multiple returns, exported = capitalized, no exceptions), then **slow down at phase 6** -
  goroutines and channels are where Go stops looking familiar and starts being Go.
- **Past the basics already?** Jump to the deep half - [Phase 10: Interfaces in Depth](10-interfaces-in-depth.md)
  onward is where Go stops being "a small, readable language" and starts being one you can reason about
  down to the scheduler.

## The phases

**Part 1 - The basics (🟢 Basic → 🟡 Intermediate)**
1. **[Install & Your First Program](01-install-and-first-program.md)** 🟢 - the Go toolchain, `hello.go`, and the refusal to compile unused code.
2. **[Syntax, Values & Types](02-syntax-values-and-types.md)** 🟢 - `var` vs `:=`, static typing, the basic types, and zero values.
3. **[Collections](03-collections.md)** 🟢 - arrays vs slices, `append`, maps, and looping with `range`.
4. **[Control Flow & Functions](04-control-flow-and-functions.md)** 🟢 - the single `for`, `if`, `switch`, and multiple return values.
5. **[Modules & Project Layout](05-modules-and-project-layout.md)** 🟢 - `go mod init`, packages, exported = capitalized, a sane layout.
6. **[Goroutines & Channels](06-goroutines-and-channels.md)** 🟡 - the reason Go exists: many things at once, safely, with `go` and channels.
7. **[Errors & I/O](07-errors-and-io.md)** 🟡 - "errors are values" (no exceptions), and reading/writing files and streams.
8. **[Ecosystem & Tooling](08-ecosystem-and-tooling.md)** 🟡 - `go test`, `go fmt`, `go vet`, modules, the batteries-included toolchain.
9. **[Idioms & Gotchas](09-idioms-and-gotchas.md)** 🟡 - how Go programmers actually write Go, and the traps that bite everyone once.

**Part 2 - Beyond the basics (🔴 Advanced)**
10. **[Interfaces in Depth](10-interfaces-in-depth.md)** 🔴 - interface values as (type, value) pairs, type assertions and switches, the nil-interface trap.
11. **[Generics & Advanced Types](11-generics-and-advanced-types.md)** 🔴 - type parameters, constraints, method sets, and when generics beat interfaces.
12. **[Concurrency Patterns](12-concurrency-patterns.md)** 🔴 - `select`, `context`, worker pools, fan-in/out, the `sync` toolbox, the race detector.
13. **[Error Handling, Deep](13-error-handling-deep.md)** 🟡 - wrapping with `%w`, `errors.Is`/`As`, sentinel and custom errors, `panic`/`recover`.
14. **[The Runtime: Scheduler, Memory & GC](14-runtime-scheduler-and-memory.md)** 🔴 - the GMP scheduler, stack vs heap, escape analysis, the garbage collector.
15. **[Testing, Benchmarks & Profiling](15-testing-benchmarks-profiling.md)** 🟡 - table-driven tests, benchmarks, `pprof`, coverage, fuzzing.
16. **[The Standard Library as Design](16-standard-library.md)** 🟡 - `io.Reader`/`Writer`, `context`, `encoding/json`, `net/http` as a masterclass.
17. **[Performance & Optimization](17-performance-and-optimization.md)** 🔴 - cutting allocations, `sync.Pool`, and profile-driven optimization.

**Finale**
18. **[Where to Go Next](18-where-to-go-next.md)** 🟢 - web, CLIs, cloud & infra, and what to actually build.

> Frameworks and big projects (gRPC, Kubernetes internals, cgo) are their own world - this guide makes
> the *language and its runtime* make sense, top to bottom.


---

# Install & Your First Program

Every language asks the same two things first: get the tools onto your machine, and prove they work with
one tiny program. With Go this is quick, and the first program already teaches you something surprising.

## What "installing Go" actually gives you

"Installing Python" usually means an interpreter that reads code line by line. Go is different: it's a
**compiled** language. Installing Go gives you a single command-line tool - `go` - that bundles a
compiler (turns your source into a standalone machine-code program), a package manager, a test runner, a
formatter, and more. One program *is* the toolchain.

📝 **Terminology.** A **compiler** translates the whole program into machine code *before* it runs. The
result is a self-contained executable - a file your OS can run directly, with no "Go" needed on the
machine that runs it. That's why a Go program ships as one binary.

## Install it

Go to the official downloads page - **[go.dev/dl](https://go.dev/dl)** - and grab the installer for your
OS (Windows `.msi`, macOS `.pkg`, or the Linux tarball). Run it and accept the defaults. The installer
puts the `go` command on your system `PATH`, so any terminal can find it.

⚠️ **Gotcha.** `go: command not found` (or `'go' is not recognized` on Windows) right after installing
usually means your terminal was already open before the install and hasn't picked up the new `PATH`.
Close it and open a fresh one.

## Confirm it works with `go version`

Before writing any code, ask Go to introduce itself:
```console
$ go version
go version go1.25.0 linux/amd64
```
The `version` sub-command reports the installed release and platform (`linux/amd64` here - yours may say
`windows/amd64`, `darwin/arm64`, etc.). The patch number will differ over time; anything `go1.22` or newer
works. A version line means you're ready.

## Write your first program

Make a file called `hello.go` in any folder, in any text editor, with exactly this:
```go
package main

import "fmt"

func main() {
	fmt.Println("Hello, Go!")
}
```
That's a complete Go program. Five lines of code, each with a specific job:

- `package main` - Go organizes all code into **packages** (named groups of related code). The package
  named `main` is special: it's the one Go turns into a runnable program. Every program you can *run*
  starts with `package main`.
- `import "fmt"` - pulls in the **`fmt`** package from Go's standard library (the toolbox that ships with
  Go). `fmt` (short for "format") holds the functions for printing text. You import what you want to use.
- `func main() { ... }` - defines a **function** named `main`. This one is also special: it's the
  *entry point*, the single function Go runs when your program starts, top to bottom from the first line.
- `fmt.Println("Hello, Go!")` - calls the `Println` ("print line") function *from* the `fmt` package
  (the dot means "the `Println` that lives in `fmt`"). It prints the text and moves to a new line.

💡 **Key point.** Two things named `main` carry all the magic of "this is a program you can run": the
**package** `main` and the **function** `main` inside it. Together they tell Go "start here." Library
code lives in differently-named packages and has no `main` function.

## Run it with `go run`

From the folder containing `hello.go`:
```console
$ go run hello.go
Hello, Go!
```
`go run` **compiled** `hello.go` into a temporary program and immediately ran it, showing the output.
It's the fastest way to try code while learning - no executable file to manage yourself. (In
[phase 5](05-modules-and-project-layout.md) you'll meet `go build`, which keeps the binary instead of
throwing it away.)

Line by line: Go entered `main`, hit the `fmt.Println` call, printed the text, reached the closing `}`,
and the program ended.

## Go's surprise: unused things are *errors*

In most languages, importing a package you don't use, or declaring a variable you never read, gets a
warning at worst - the program still runs. **In Go, both are hard compile errors. Your program will not
build.**

Watch what happens if we import `fmt` but never call it:
```go
package main

import "fmt"

func main() {
}
```
```console
$ go run hello.go
./hello.go:3:8: "fmt" imported and not used
```
The compiler refused to build the program *at all*, pointing straight at the offending line
(`hello.go:3:8` means file `hello.go`, line 3, column 8) with the exact problem: `"fmt" imported and not
used`. No binary was produced. Same story for a variable you declare and never read (you'll see `declared
and not used` - covered in [phase 2](02-syntax-values-and-types.md)).

⚠️ **Gotcha.** This *feels* hostile the first few times, especially mid-edit when you've commented out
the line that used an import. It's not a bug and there's no flag to turn it off - it's deliberate.

**Why Go made this choice.** Unused imports and dead variables are how real codebases slowly rot:
mysterious dependencies, leftover names that mislead the next reader. Making them *errors* guarantees
every import is needed and every variable is used. The fix is always trivial - delete the unused line, or
actually use the thing. Once internalized, it stops registering as friction.

## Recap

1. **Go is compiled**; installing it from [go.dev/dl](https://go.dev/dl) gives you one `go` command that
   is the whole toolchain.
2. **`go version`** confirms the install and tells you the release and platform.
3. A runnable program needs **`package main`** and a **`func main()`** - that pair is the entry point.
4. **`import`** pulls in packages like **`fmt`**; you call into them with `package.Function`, e.g.
   `fmt.Println(...)`.
5. **`go run file.go`** compiles and runs in one step - perfect for learning.
6. **Unused imports and unused variables are compile errors in Go**, on purpose, to keep code clean.

Next: named values, their types, and the zero-value rule - Go variables are never mysteriously
uninitialized.


---

# Syntax, Values & Types

Real programs hold values - a name, a count, a price - and do things with them. In Go, every value has a
**type**, and the language is strict about it in a way that feels heavy at first but becomes a safety net
fast.

## What "statically typed" actually means

Go is **statically typed**: every variable has a fixed type, known and checked when your code is
*compiled*, before it ever runs. A variable holding a number can never later hold text - the compiler
verifies this up front.

Coming from Python or JavaScript, you might expect a variable to be a box you can drop anything into. In
Go it's a box with a *shape* - number-shaped, text-shaped - and the compiler checks everything you put in
fits. Bugs like "I thought this was a number but it was the text `'42'`" can't survive to runtime; they're
caught while you build.

📝 **Terminology.** A **type** is the kind of value something is - an integer, a piece of text, a
true/false. **Static** means "checked at compile time" (vs. *dynamic*, "checked while running"). Go
checks early, so the running program never has to wonder what type something is.

## `var` - the explicit way to declare a value

The full, spelled-out way to create a variable uses the `var` keyword:
```go
package main

import "fmt"

func main() {
	var name string = "Ada"
	var age int = 36
	fmt.Println(name, "is", age)
}
```
```console
$ go run main.go
Ada is 36
```
`var name string = "Ada"` reads left to right as "declare a variable called `name`, of type `string`
(text), and set it to `"Ada"`." Same for `age`, of type `int`. `fmt.Println` printed all three pieces with
a space between each - the types are written right there, so there's no ambiguity about what each box
holds.

Go can usually figure the type out from the value, so you can drop it and let the compiler *infer* it:
```go
var name = "Ada"   // Go sees "Ada" is text, so name is a string
var age = 36       // Go sees 36 is a whole number, so age is an int
```
`name` is still a `string`, `age` still an `int` - you just didn't spell it out. The variable is no less
typed; the compiler inferred it.

## `:=` - the short declaration you'll use most

Inside a function, Go gives you an even shorter form that *declares and assigns at once*:
```go
package main

import "fmt"

func main() {
	name := "Ada"
	age := 36
	fmt.Println(name, "is", age)
}
```
`name := "Ada"` is the **short variable declaration**. The `:=` means "create a new variable and set it" -
Go infers the type from the value, exactly like `var name = "Ada"` but more compact. This is the form
you'll see and write most in real Go code.

⚠️ **Gotcha - `:=` only works inside functions.** At the top level of a file, you *must* use `var`.
Writing `count := 0` outside a function is a compile error: `:=` is shorthand for a local working
variable, and the top level is for declaring package-level things, `var`'s job. Rule of thumb:
**inside `func` → `:=`; outside → `var`.**

## The basic types you'll actually use

These cover almost everything early on:

- **`int`** - a whole number, positive or negative (`-3`, `0`, `42`). Your default for counting.
- **`float64`** - a number with a decimal point (`3.14`, `-0.5`). The `64` is how many bits it uses; the
  normal choice for fractional numbers.
- **`string`** - text, written in double quotes (`"hello"`). 
- **`bool`** - a truth value, either `true` or `false`. Named after George Boole.

There are more (smaller and larger integers, an unsigned variety, a single-character `rune`), but reach
for those only with a specific reason. `int`, `float64`, `string`, and `bool` will take you a long way.

## Zero values - why Go variables are never "undefined"

In some languages, a variable you declare but don't set holds garbage, or a `null`/`undefined` that blows
up the moment you touch it. **Go refuses to leave a variable empty.** Every type has a defined **zero
value**, and a freshly declared variable starts there automatically.

```go
package main

import "fmt"

func main() {
	var count int
	var price float64
	var label string
	var ready bool
	fmt.Println(count, price, label, ready)
}
```
```console
$ go run main.go
0 0  false
```
Four variables, none set, yet all start at sensible, predictable values: `int` at `0`, `float64` at `0`,
`string` at `""` (the blank gap between `0` and `false` in the output), `bool` at `false`. Nothing is
"undefined" - you can read any of them immediately without a crash.

💡 **Key point.** The zero value rule means **there is no uninitialized-variable surprise in Go.** Declare
`var count int` and you *know* it's `0` - a whole class of "why is this null?" bugs never happens. Worth
memorizing: numbers → `0`, strings → `""`, bools → `false`. (We'll meet `nil` - the zero value for a few
special types - in [phase 3](03-collections.md); it follows the same rule.)

## The other unused-thing error: `declared and not used`

You met unused *imports* in [phase 1](01-install-and-first-program.md). Variables have the same rule:
declare one and never read it, and Go won't compile.
```go
package main

func main() {
	total := 100
}
```
```console
$ go run main.go
./main.go:4:2: declared and not used: total
```
`total` was never used, so the compiler stopped and pointed at it (`main.go:4:2`). Fix: use the variable
(print it, return it, calculate with it) or delete the line - same philosophy as unused imports.

## Printing properly with `Println` and `Printf`

You've been using `fmt.Println`, which prints its arguments with spaces between them and a trailing
newline - great for quick output. For *control* over the format, reach for `fmt.Printf` ("print
formatted"):
```go
package main

import "fmt"

func main() {
	name := "Ada"
	age := 36
	fmt.Printf("%s is %d years old.\n", name, age)
}
```
```console
$ go run main.go
Ada is 36 years old.
```
`Printf` takes a *format string* with **verbs** - placeholders starting with `%` - filled in by the values
that follow, in order. `%s` means "put a string here," `%d` means "put a whole number (a *decimal*
integer) here," and `\n` is the newline character (`Printf`, unlike `Println`, doesn't add one for you).
Verbs worth knowing now:

- `%s` - a string
- `%d` - an integer
- `%f` - a float (`%.2f` rounds to 2 decimal places)
- `%v` - *any* value in its default form (the helpful catch-all when you're not sure)
- `%t` - a boolean (`true`/`false`)

⚠️ **Gotcha.** Mismatched verbs don't crash Go - it prints a visible complaint right in the output, like
`%!d(string=Ada)`, meaning "you asked for a `%d` integer but handed me the string `Ada`." Ugly on
purpose, so you spot the mismatch instantly instead of shipping wrong output.

## Recap

1. Go is **statically typed**: every variable has a fixed type, checked when you compile.
2. **`var name type = value`** is the explicit declaration; drop the type to let Go **infer** it.
3. **`:=`** declares-and-assigns in one step and is what you'll use most - but **only inside functions**;
   use `var` at the top level.
4. The everyday types: **`int`, `float64`, `string`, `bool`**.
5. **Zero values** mean nothing is ever uninitialized: numbers `0`, strings `""`, bools `false`.
6. **Unused variables are compile errors** (`declared and not used`), just like unused imports.
7. **`fmt.Println`** for quick output; **`fmt.Printf`** with verbs (`%s %d %f %v %t`) for formatted
   output.

Next: from single values to *collections* - arrays, the slices you'll actually use, and maps for looking
things up by name.


---

# Collections

So far you've held one value at a time. Real programs deal in *many* - a list of users, a table of
prices, the words in a sentence. One decision trips up newcomers: arrays versus slices. Clear that up
first, and everything else falls into place.

## Arrays vs slices - the distinction that matters

An **array** in Go is a fixed-size sequence of values, all the same type. The size is part of the type:
`[3]int` is "exactly three integers" - not two, not four. You can't grow it, which makes arrays rare in
everyday Go.

A **slice** is a *flexible-length* view onto a sequence of values. It can grow and shrink, and it's what
Go programmers use almost all the time - "a list that can change size." Its type has no number: `[]int`
is "a list of integers, however many."

📝 **Terminology.** The empty brackets are the tell. **`[3]int`** (number inside) = array, fixed.
**`[]int`** (nothing inside) = slice, flexible. When in doubt, you want the slice.

Here's a slice in action:
```go
package main

import "fmt"

func main() {
	primes := []int{2, 3, 5, 7}
	fmt.Println(primes)
	fmt.Println(primes[0], primes[3])
}
```
```console
$ go run main.go
[2 3 5 7]
2 7
```
`[]int{2, 3, 5, 7}` created a slice of four integers, printed in brackets. `primes[0]` reads the **first**
element (Go counts from zero), `primes[3]` the fourth - so we printed `2` then `7`. Indexing past the end
(say `primes[4]`) crashes with an out-of-range error, since there's no fifth element.

## Growing a slice with `append`

A slice's whole point is that it can grow. You do that with the built-in `append` function:
```go
package main

import "fmt"

func main() {
	names := []string{"Ada", "Alan"}
	names = append(names, "Grace")
	fmt.Println(names)
}
```
```console
$ go run main.go
[Ada Alan Grace]
```
`append(names, "Grace")` produced a slice with `"Grace"` added on the end. The surprise: **you assign the
result back to `names`.** `append` doesn't always change the original in place - it may build a bigger
slice and hand it back - so the idiom is *always* `names = append(names, ...)`. Skip the `names =` and
your addition vanishes.

You can append several at once, or even append one slice onto another:
```go
names = append(names, "Linus", "Margaret")
```
`append` takes the slice first, then any number of new values, returning the grown slice. Same rule:
capture the result.

## `len` and `cap` - length vs capacity

Two built-in functions tell you about a slice's size:
```go
package main

import "fmt"

func main() {
	s := []int{10, 20, 30}
	fmt.Println(len(s), cap(s))
}
```
```console
$ go run main.go
3 3
```
`len(s)` is the **length** - how many elements the slice holds right now (3). `cap(s)` is the
**capacity** - how many it could hold before Go must allocate a bigger block of memory. **`len` is the one
you'll use constantly**; `cap` is under-the-hood detail you'll mostly ignore until optimizing. They start
equal here, but after appends they can differ as Go grows backing storage in chunks.

## Maps - looking things up by key

A slice is great for an ordered list, accessed by *position*. A **map** is for accessing things by *name*:
a lookup table storing **key → value** pairs. `map[string]int` reads as "a map from string keys to
integer values" - names to ages, say. (Other languages call this a dictionary, hash, or associative
array; same idea.)
```go
package main

import "fmt"

func main() {
	ages := map[string]int{
		"Ada":  36,
		"Alan": 41,
	}
	fmt.Println(ages["Ada"])
	ages["Grace"] = 28
	fmt.Println(ages)
}
```
```console
$ go run main.go
36
map[Ada:36 Alan:41 Grace:28]
```
We created a map with two entries, looked up `"Ada"` to get `36`, then added an entry by assigning to a
fresh key (`ages["Grace"] = 28`). Go printed them in tidy order here (because `fmt` sorts map keys when printing), but **maps have no
guaranteed order** - ranging over a map visits keys in randomized order, so don't rely on it.

When you look up a key that might not exist, use the **two-value form** to ask "did it exist?":
```go
age, ok := ages["Nobody"]
fmt.Println(age, ok)
```
```console
0 false
```
Reading a missing key doesn't crash - it returns the value type's **zero value** (`0` for an `int`, from
[phase 2](02-syntax-values-and-types.md)) plus a boolean, `ok`, `false` when the key was absent. The
`value, ok := m[key]` pattern distinguishes "the value is genuinely 0" from "the key wasn't there at all."

⚠️ **Gotcha - writing to a nil map panics.** A map variable declared but never *made* is `nil`, and
**writing to a nil map crashes your program at runtime:**
```go
var m map[string]int   // declared, but nil - never made
m["x"] = 1             // panic!
```
```console
panic: assignment to entry in nil map
```
`var m map[string]int` gives you a `nil` map - the zero value for maps. You can *read* from it (zero
values come back), but *writing* panics, since no table is allocated to store into. Fix: create it first
with `make`: `m := make(map[string]int)` (or a map literal like above). One of the most common first-week
Go panics - now you'll recognize it instantly.

📝 **Terminology.** A **panic** is Go's term for a runtime crash - the program stops with an error message
and a trace, the runtime equivalent of an exception. Handling failure *gracefully* (the "errors are
values" approach) comes in [phase 7](07-errors-and-io.md).

## Looping over collections with `range`

To visit every element of a slice or every pair in a map, Go gives you `range`:
```go
package main

import "fmt"

func main() {
	names := []string{"Ada", "Alan", "Grace"}
	for i, name := range names {
		fmt.Println(i, name)
	}
}
```
```console
$ go run main.go
0 Ada
1 Alan
2 Grace
```
`for i, name := range names` walks the slice, handing you `i`, the **index** (starting at 0), and `name`,
the **value** at that position - the standard way to loop a slice. (Don't worry about the `for` keyword
yet - Go's single loop is [phase 4](04-control-flow-and-functions.md); here it's just "do this for each
element.")

Often you only want the value, not the index. Use the blank identifier `_` to throw the index away:
```go
for _, name := range names {
	fmt.Println(name)
}
```
`_` is Go's "I deliberately don't want this" placeholder. Since Go errors on *unused variables*
([phase 2](02-syntax-values-and-types.md)), `_` says "discard the index on purpose." Ranging a map works
the same way, giving `key, value` instead of `index, value`.

## The slice-aliasing surprise

Here's the slice gotcha that bites everyone exactly once. A slice is a *view* onto an underlying block of
data. When you slice a slice, both names can point at the *same* underlying data:
```go
package main

import "fmt"

func main() {
	original := []int{1, 2, 3, 4}
	part := original[0:2]   // a view of the first two elements
	part[0] = 99
	fmt.Println(original)
}
```
```console
$ go run main.go
[99 2 3 4]
```
`original[0:2]` made `part` a *window* onto `original`'s first two elements - not a copy. Changing
`part[0]` also changed `original[0]`; they share the same backing storage. Efficient (no copying) but
surprising the first time a slice changes "by itself."

⚠️ **Gotcha.** When you need an *independent* copy rather than a shared view, make one explicitly with the
built-in `copy`:
```go
clone := make([]int, len(original))
copy(clone, original)
```
`make([]int, len(original))` created a new slice of the same length, and `copy` filled it with
`original`'s values. Now `clone` has its own storage - changing it leaves `original` untouched. Reach for
this whenever "I changed one and the other changed too" would be a bug.

## Recap

1. **`[]T` is a slice** (flexible, what you'll use); `[N]T` is an array (fixed size, rare).
2. **`append`** grows a slice - always assign the result back: `s = append(s, x)`.
3. **`len`** is how many elements (you'll use it constantly); **`cap`** is the under-the-hood capacity.
4. A **map** (`map[K]V`) stores key→value pairs for instant lookup; use **`value, ok := m[key]`** to
   check if a key exists.
5. **Writing to a nil map panics** - create it first with `make` or a literal.
6. **`range`** loops collections, giving `index, value` for slices and `key, value` for maps; use `_` to
   discard a part you don't need.
7. Slices can **share underlying data** (aliasing) - use `copy` when you need an independent copy.

Next: making decisions and organizing logic - Go's one loop, `if` and `switch`, and the
multiple-return-value functions that give Go its distinctive shape.


---

# Control Flow & Functions

Up to now your programs run straight down, top to bottom. Real logic *branches* (do this if that),
*repeats* (do this for each item), and is *organized into reusable pieces* (functions). Go's take on all
three is lean - one loop, not three - and functions can hand back more than one value at a time, a
feature that shapes how *all* Go code reads.

## `if` - branching, with a twist

The basic `if` looks like you'd expect, with one Go quirk: **no parentheses around the condition, but
braces are mandatory.**
```go
package main

import "fmt"

func main() {
	age := 20
	if age >= 18 {
		fmt.Println("adult")
	} else {
		fmt.Println("minor")
	}
}
```
```console
$ go run main.go
adult
```
`if age >= 18` checked the condition; since `20 >= 18` is true, the first block ran and printed `adult`.
No `( )` around the condition (Go doesn't use them), but the `{ }` are required even for a single line -
this prevents the classic bug where an unbraced `if` silently covers only one statement.

Go's distinctive touch is the **`if` with an init statement** - you can declare a variable right in the
`if`, scoped to just that block:
```go
if n := len("hello"); n > 3 {
	fmt.Println("long word, length", n)
}
```
```console
long word, length 5
```
`if n := len("hello"); n > 3` does two things separated by the semicolon: declares `n` (the length, 5),
then tests `n > 3`. `n` exists *only* inside the `if`/`else` blocks and vanishes after, keeping short-lived
helper values from leaking out. You'll see this constantly with error checks in
[phase 7](07-errors-and-io.md).

## `for` - Go's one and only loop

Here's a genuine surprise: **Go has exactly one loop keyword, `for`.** No `while`, no `do-while`, no
separate `foreach` statement. The designers decided one flexible loop was clearer than four, and `for`
shape-shifts to cover every case.

The classic counting loop:
```go
package main

import "fmt"

func main() {
	for i := 0; i < 3; i++ {
		fmt.Println(i)
	}
}
```
```console
$ go run main.go
0
1
2
```
`for i := 0; i < 3; i++` has three parts separated by semicolons: **init** (`i := 0`, once), **condition**
(`i < 3`, checked before each pass), **post** (`i++`, after each pass; adds one to `i`). Prints `0`, `1`,
`2`, stopping when `i` hits `3`.

Drop the init and post, keep only a condition, and the same `for` becomes a "while" loop:
```go
n := 3
for n > 0 {
	fmt.Println(n)
	n--
}
```
```console
3
2
1
```
`for n > 0` loops as long as the condition holds - what other languages spell `while (n > 0)`. Go reuses
`for`. (`n--` subtracts one from `n`.) Drop the condition too - `for { ... }` - for an infinite loop,
exited with `break` or `return`.

You've already seen the third form: `for ... range` over a collection, in [phase 3](03-collections.md).
One keyword, three shapes.

💡 **Key point.** Whenever you'd reach for `while` elsewhere, in Go you write `for condition` - it's
always `for`.

## `switch` - cleaner than a stack of `if`s

When you're comparing one value against several options, `switch` reads better than a tower of
`else if`:
```go
package main

import "fmt"

func main() {
	day := "Sat"
	switch day {
	case "Sat", "Sun":
		fmt.Println("weekend")
	case "Fri":
		fmt.Println("almost there")
	default:
		fmt.Println("weekday")
	}
}
```
```console
$ go run main.go
weekend
```
`switch day` compared `day` against each `case`, matching `"Sat"` in the first case (which lists two
values, either matches) and printing `weekend`. `default` runs when nothing else matches.

⚠️ **Gotcha (the good kind) - Go's `switch` does not fall through.** In C, Java, or JavaScript, you need
`break` at the end of every `case` or execution falls through into the next one. **Go is the opposite:
each case stops on its own**, no `break` needed - no more "forgot the `break` and three cases ran." (An
explicit `fallthrough` keyword exists for when you *want* it, but you'll rarely need it.)

## Functions - and the multiple-return signature that defines Go

A **function** is a named, reusable block that takes inputs (**parameters**) and hands back outputs
(**return values**). Here's one that adds two numbers:
```go
package main

import "fmt"

func add(a int, b int) int {
	return a + b
}

func main() {
	fmt.Println(add(3, 4))
}
```
```console
$ go run main.go
7
```
`func add(a int, b int) int` reads as "a function named `add`, taking two `int` parameters, and
*returning* an `int`." The return type sits **after** the parameters - takes a moment if you're used to
`int add(...)`, but reads naturally left-to-right once it clicks. `return a + b` hands the sum back, and
`main` printed it.

Now the feature that shapes all of Go: **a function can return more than one value** - most importantly,
a result *and* whether it failed:
```go
package main

import "fmt"

func divide(a, b int) (int, bool) {
	if b == 0 {
		return 0, false   // can't divide by zero - signal failure
	}
	return a / b, true
}

func main() {
	result, ok := divide(10, 2)
	fmt.Println(result, ok)

	result, ok = divide(10, 0)
	fmt.Println(result, ok)
}
```
```console
$ go run main.go
5 true
0 false
```
`func divide(a, b int) (int, bool)` returns *two* values - the result and a boolean for whether it worked.
(`a, b int` is shorthand for "both are `int`.") The caller catches both with `result, ok := divide(...)`.
Dividing by zero returned `0, false` instead of crashing, letting the caller check `ok` and react. This
`(value, ok)` or - far more commonly - `(value, error)` shape is *the* Go signature, seen on nearly every
function that can fail, and why Go doesn't need exceptions - unpacked in [phase 7](07-errors-and-io.md).

The shape of code you'll write hundreds of times:

```mermaid
flowchart TD
  Call[Call divide a, b] --> Check{b == 0?}
  Check -- yes --> Fail[return 0, false]
  Check -- no --> Ok[return a/b, true]
  Fail --> Caller{caller checks ok}
  Ok --> Caller
  Caller -- ok is false --> Handle[handle the failure]
  Caller -- ok is true --> Use[use the result]
```

## `defer` - a teaser for cleanup done right

One more keyword you'll meet constantly: **`defer`**. It schedules a function call to run *when the
surrounding function is about to return*, no matter how it returns:
```go
package main

import "fmt"

func main() {
	defer fmt.Println("goodbye")
	fmt.Println("hello")
}
```
```console
$ go run main.go
hello
goodbye
```
`defer fmt.Println("goodbye")` didn't run immediately - Go *deferred* it until `main` was finishing. So
`hello` printed first, then `goodbye` ran on the way out. `defer` is everywhere because it guarantees
cleanup: when you open a file or connection, you `defer` closing it right next to opening it, and Go runs
the close no matter which path the function takes out. More in [phase 7](07-errors-and-io.md); for now,
it means "run this last, guaranteed."

## Recap

1. **`if`** uses no parentheses but requires braces; its **init form** (`if x := …; cond`) scopes a
   helper variable to the block.
2. Go has **one loop, `for`** - it covers counting (`for i := 0; …`), while (`for cond`), infinite
   (`for {}`), and `for … range`.
3. **`switch`** compares a value against cases and **does not fall through** - no `break` needed.
4. **Functions** put the return type after the parameters; **multiple return values** (especially
   `(value, error)`) are the defining Go signature.
5. **`defer`** schedules a call to run when the function returns - the idiomatic way to guarantee cleanup.

Next: building *projects* - modules, packages, why a capital letter makes something public, and a sane
layout, the groundwork for the goroutines in phase 6.


---

# Modules & Project Layout

One file is fine for learning. A real program is many files, often using code other people wrote, and
needs a way to say "this project depends on *that* library, at *this* version." That's what **modules**
and **packages** are for - get these two ideas straight and your project stops being a pile of `.go`
files and becomes something you can grow and ship as a single binary.

## Packages vs modules - two words people mix up

📝 **Terminology.**
- A **package** is a folder of `.go` files that belong together and share a name (you've already used the
  `fmt` package and written `package main`) - the unit of *code organization*, one folder, one package.
- A **module** is a whole *project*: a collection of packages versioned and released together, with one
  file (`go.mod`) recording its name and dependencies - the unit of *distribution and versioning*.

In short: **a module is your project; packages are the folders inside it.** A tiny program is one module,
one package; a big one is one module, many.

## `go mod init` - create the module

Start a project by making a folder and initializing a module in it:
```console
$ mkdir greeter
$ cd greeter
$ go mod init example.com/greeter
go: creating new go.mod: module example.com/greeter
```
`go mod init example.com/greeter` created a `go.mod` file marking this folder as a module named
`example.com/greeter`. That name is the module's **import path** - the unique address other code uses to
import it. Convention is a URL-like path (often where the code lives, e.g. `github.com/you/greeter`); for
a local-only project, anything unique works.

Peek at it:
```console
$ cat go.mod
module example.com/greeter

go 1.25
```
`go.mod` is small and human-readable: the module's name and the Go version it targets. Third-party
libraries get listed here too once added - the single source of truth for "what does my project depend
on." You rarely edit it by hand; the `go` tool keeps it updated.

## Exported = Capitalized - Go's whole visibility rule

Most languages use keywords like `public` and `private` to control what other code can see. **Go uses
capitalization instead, and that's the entire rule:**

> An identifier (a function, type, or variable name) that starts with a **Capital letter** is
> **exported** - visible to other packages. One that starts with a **lowercase letter** is **unexported**
> - private to its own package.

That's it, no keywords. Make a package folder `greeting` with a file `greeting.go`:
```go
package greeting

import "fmt"

// Hello is exported - capital H - so other packages can call it.
func Hello(name string) string {
	return fmt.Sprintf("Hello, %s!", greetingPrefix(name))
}

// greetingPrefix is unexported - lowercase g - private to this package.
func greetingPrefix(name string) string {
	return name
}
```
`Hello` starts with a capital `H`, so any package that imports `greeting` can call `greeting.Hello(...)`.
`greetingPrefix` starts lowercase - an internal helper, invisible outside this package even sitting right
there in the file. (`fmt.Sprintf` is like `Printf` but *returns* the formatted string instead of printing
it.)

💡 **Key point.** When you see `thing.DoStuff()`, the capital `D` is *why* you're allowed to call it.
Capitalize to make something part of your package's public surface; keep it lowercase to keep it an
implementation detail you're free to change later - a one-letter decision made on purpose.

## Importing your own package

Now use that package from your program. In the module root, a `main.go`:
```go
package main

import (
	"fmt"

	"example.com/greeter/greeting"
)

func main() {
	fmt.Println(greeting.Hello("Ada"))
}
```
```console
$ go run .
Hello, Ada!
```
The `import (...)` block (parentheses let you import several at once) brings in both the standard-library
`fmt` and *your own* `greeting` package, addressed by its full path: the module name `example.com/greeter`
plus the folder `greeting`. `greeting.Hello("Ada")` then calls the exported function. We ran `go run .` (a
dot, "this whole package") rather than naming a single file - once a project has multiple files, `.` tells
Go to build the package in the current folder, all of it together.

⚠️ **Gotcha.** The import path is **module name + folder path**, not just the folder name. With module
`example.com/greeter` and folder `greeting/`, the import is `"example.com/greeter/greeting"` - just
`"greeting"` won't resolve. The *package name* (`package greeting`) is what you type to use it
(`greeting.Hello`); keep folder and package names the same to avoid confusion.

## `go build` vs `go run`

`go run` compiles and runs in one disposable step - ideal while iterating. For the actual *program* - a
standalone executable to keep, ship, or deploy - use `go build`:
```console
$ go build
$ ls
go.mod  greeter  greeting  main.go
$ ./greeter
Hello, Ada!
```
`go build` compiled the module into a single executable named after its last path segment (`greeter` here;
`greeter.exe` on Windows). Running `./greeter` ran it directly - no `go` involved. That binary is
self-contained: copy it to another machine of the same OS and CPU type and it just runs, no Go installed there.
**`go run` = try it now; `go build` = produce the thing you ship.**

## A sane small layout

You don't need an elaborate folder structure to start - resist the urge to over-organize a small project.
A clean starting shape for a program with a bit of internal code:

```mermaid
flowchart TD
  Mod["greeter/ (module: example.com/greeter)"]
  Mod --> GoMod["go.mod - name + dependencies"]
  Mod --> Main["main.go - package main, the entry point"]
  Mod --> Pkg["greeting/ - package greeting"]
  Pkg --> PkgFile["greeting.go - Hello (exported)"]
```

The module root holds `go.mod` and your `package main` (the entry point); each subfolder is one package of
supporting code, grouped by what it does. As the project grows, add more package folders the same way -
each a self-contained unit with a clear public surface (capitalized names) and private internals
(lowercase). Start flat; split into packages only when a real grouping emerges.

## Looking ahead to concurrency

You now have the whole foundation: values and types, collections, control flow, multiple-return
functions, and a project structured into a module and packages.

So far every program has done **one thing at a time**, top to bottom. But the reason companies reach for
Go - the reason it powers Docker, Kubernetes, and a huge slice of the cloud - is how gracefully it does
*many* things at once: thousands of network connections, work in parallel, a server that stays responsive
under load. That's **concurrency**, and Go's tools for it (`goroutines` and `channels`) are next.

## Recap

1. A **package** is a folder of related code; a **module** is the whole project, defined by `go.mod`.
2. **`go mod init <path>`** creates the module and its `go.mod` (the record of name + dependencies).
3. **Exported = Capitalized.** Capital-first identifiers are public to other packages; lowercase-first are
   private - that's Go's entire visibility rule.
4. **Import your own packages** by `module-name/folder-path`, and run multi-file packages with `go run .`.
5. **`go run`** compiles-and-runs disposably; **`go build`** produces a standalone executable to ship.
6. **Start flat**, split into package folders only when a real grouping appears.

Next: goroutines and channels - doing many things at once, safely. The reason Go exists.


---

# Goroutines & Channels - Go's Concurrency

Concurrency is where a lot of languages make you feel stupid - threads, locks, mutexes, a race condition you can't reproduce. Go's pitch: two small ideas, designed to fit together, that make this *approachable*.

A **goroutine** is a task running alongside your other tasks, cheap enough to start thousands of. A **channel** is a pipe between tasks: one goroutine puts a value in, another takes it out. The famous Go mantra falls out of those two facts:

> **Don't communicate by sharing memory; share memory by communicating.**

Instead of two tasks poking at the same variable behind a lock (and racing each other), you hand the value down a channel from one to the other. Whoever holds it owns it. No lock, no race.

## Goroutines: a concurrent task, started with one word

A goroutine is a function that runs concurrently with the rest of your program. Put the word `go` in front of a function call to start one - the call returns immediately while the function runs on its own.

📝 **Terminology.** A *goroutine* is not an operating-system thread. It's a lightweight task the Go runtime schedules onto a small pool of real threads. It starts with a tiny stack (a couple of kilobytes) that grows as needed, which is why running thousands is normal and cheap - something you can't do with OS threads. (For what a thread actually is underneath, see [What Happens When Code Runs](/guides/what-happens-when-code-runs).)

```go
package main

import (
	"fmt"
	"time"
)

func main() {
	go fmt.Println("hello from the goroutine")
	fmt.Println("hello from main")
	time.Sleep(10 * time.Millisecond) // give the goroutine a moment to run
}
```
```console
$ go run main.go
hello from main
hello from the goroutine
```
`go fmt.Println(...)` launched that print as a separate task and returned instantly, so `main` printed its own line first, then the goroutine got a turn. The `time.Sleep` is a crude hack: without it, `main` would reach the end and exit *before* the goroutine ever ran (`main` ending kills every goroutine with it). That sleep is a placeholder - channels and `WaitGroup`, below, are the real way to wait.

⚠️ **Gotcha.** When `main` returns, the program exits immediately and takes all goroutines with it, finished or not. A goroutine isn't a promise the work will complete - it's a request to run *alongside*, and you need an explicit way to wait for it.

## Channels: a typed pipe between goroutines

A channel is a pipe you send values into and receive values out of - every channel carries one specific type. A `chan int` carries `int`s; a `chan string` carries `string`s. Make one with `make`, send with `ch <- v`, receive with `v := <-ch`.

**Why this is the heart of it.** A channel does two jobs at once: it *moves a value* from one goroutine to another, and it *synchronizes* them. On a plain (unbuffered) channel, a send blocks until someone is ready to receive, and a receive blocks until someone is ready to send - a handshake. When the value comes out the other end, you *know* the sender reached that point. Coordination for free, no lock needed.

Picture the handshake - one goroutine produces a number, sends it down the channel, and `main` receives it:

```mermaid
sequenceDiagram
  participant M as main
  participant W as worker goroutine
  M->>W: go work(ch)
  W->>W: compute result
  W-->>M: ch <- 42  (send blocks until received)
  M->>M: v := <-ch  (receives 42)
  Note over M,W: the send/receive is one handshake
```

The proper version of "wait for the goroutine," using a channel instead of `time.Sleep`:
```go
package main

import "fmt"

func work(ch chan int) {
	ch <- 42 // send the result into the channel
}

func main() {
	ch := make(chan int) // an unbuffered channel of ints
	go work(ch)
	v := <-ch // blocks here until work() sends
	fmt.Println("got", v)
}
```
```console
$ go run main.go
got 42
```
`main` started `work` as a goroutine, then sat on `<-ch`, blocked, waiting. `work` computed its result and sent `42` into the channel; `main` woke, received the value, and printed it. No `Sleep`, no race - the channel made `main` wait for exactly the right moment. Value and timing, coordinated in one line each.

📝 **Terminology.** An *unbuffered* channel (`make(chan int)`) holds nothing - a send waits for a receiver, hand-to-hand. A *buffered* channel (`make(chan int, 3)`) has room for a few values, so a send only blocks when the buffer is full. Start with unbuffered; reach for a buffer only when you have a reason.

⚠️ **Gotcha - deadlock on an unbuffered channel.** Because an unbuffered send blocks until *someone* receives, sending on a channel that nobody is listening to freezes forever. Go's runtime can often detect when *every* goroutine is stuck and bails out:
```go
func main() {
	ch := make(chan int)
	ch <- 1 // nobody is receiving - this blocks forever
}
```
```console
$ go run main.go
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
	/tmp/main.go:3 +0x28
exit status 2
```
`main` tried to send `1`, but no goroutine was ready to receive, so the send blocked. Since `main` was the only goroutine and now stuck, nothing could progress - the runtime recognized everyone was asleep and reported `deadlock!`. Fix: make sure a receiver exists (start it as a goroutine *before* you send, or use a buffered channel if a send genuinely shouldn't wait).

## Closing a channel and ranging over it

When a sender is done, it can `close(ch)` to signal "no more values are coming." A receiver loops with `for v := range ch` to pull values until the channel is closed and drained, then stops cleanly.

```go
package main

import "fmt"

func main() {
	ch := make(chan int)
	go func() {
		for i := 1; i <= 3; i++ {
			ch <- i
		}
		close(ch) // tell the receiver we're done
	}()

	for v := range ch { // loops until ch is closed and empty
		fmt.Println("received", v)
	}
}
```
```console
$ go run main.go
received 1
received 2
received 3
```
The goroutine sent `1, 2, 3` then `close(ch)`. The `for ... range ch` loop in `main` received each value as it arrived, and when the channel was closed and emptied, the loop ended on its own - no counter, no sentinel value. Closing is how a sender says "that's all"; `range` is how a receiver listens for it.

⚠️ **Gotcha.** Only the *sender* should close a channel, and only once. Closing an already-closed channel, or sending on a closed channel, panics. Rule of thumb: whoever owns the sending end owns the close.

## `select`: waiting on several channels at once

`select` is like a `switch`, but its cases are channel operations. It blocks until *one* case can proceed, then runs it - how a goroutine listens to multiple channels, "whichever speaks first."

A common shape: do some work, but give up if it takes too long.
```go
package main

import (
	"fmt"
	"time"
)

func main() {
	result := make(chan string)
	go func() {
		time.Sleep(2 * time.Second) // pretend this is slow work
		result <- "done"
	}()

	select {
	case r := <-result:
		fmt.Println("got result:", r)
	case <-time.After(1 * time.Second):
		fmt.Println("timed out waiting")
	}
}
```
```console
$ go run main.go
timed out waiting
```
`select` waited on two channels: `result`, and the one returned by `time.After`, which delivers a value after the given delay. The work took 2 seconds but the timeout fired at 1, so the `time.After` case won. `select` is your tool for "wait for whichever happens first" - results, timeouts, cancellation, all at once.

## `sync.WaitGroup`: wait for a batch of goroutines to finish

Sometimes you don't need to pass values back - just launch goroutines and wait until *all* are done. A `sync.WaitGroup` is a counter for that: `Add` how many you're starting, each goroutine calls `Done` when it finishes, and `Wait` blocks until the counter hits zero.

```go
package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	for i := 1; i <= 3; i++ {
		wg.Add(1) // one more goroutine to wait for
		go func(id int) {
			defer wg.Done() // mark this one done on the way out
			fmt.Println("worker", id, "finished")
		}(i)
	}
	wg.Wait() // block until all three call Done
	fmt.Println("all workers finished")
}
```
```console
$ go run main.go
worker 3 finished
worker 1 finished
worker 2 finished
all workers finished
```
Each iteration called `wg.Add(1)` before launching a worker, and each worker called `wg.Done()` (via `defer`, so it runs no matter how the function exits). `wg.Wait()` held `main` until the counter dropped to zero. Workers ran in whatever order the scheduler picked (here `3, 1, 2`), but `"all workers finished"` is guaranteed to print last, since `Wait` doesn't return until all report in.

💡 **Key point.** `defer wg.Done()` is the safe habit - it guarantees the counter decrements even if the goroutine returns early or panics. Forget a `Done` and `Wait` blocks forever; call it twice and the counter goes negative and panics. One `Add`, one `Done`, per goroutine.

⚠️ **Gotcha - goroutine leaks.** A goroutine that blocks forever never gets cleaned up - it sits holding memory for the life of the program. Usual cause: a goroutine waiting to send on (or receive from) a channel nothing will ever touch again:
```go
func leak() {
	ch := make(chan int)
	go func() {
		val := <-ch // waits forever - nobody ever sends
		fmt.Println(val)
	}()
	// function returns; the goroutine is stranded, blocked, leaked
}
```
`leak` started a goroutine that blocks on `<-ch`, then returned without sending anything. The goroutine can never progress or exit - it's leaked. Unlike the all-stuck case earlier, the runtime *won't* warn you here, since the rest of the program keeps running. Leaks are silent. Fix: always give a blocked goroutine a way out - close the channel, or use a `select` with a cancellation/timeout case.

## Recap

1. **Goroutine** - a cheap concurrent task; start it with `go f()`. Thousands are fine. When `main` exits, they all die, so you need a way to wait.
2. **Channel** - a typed pipe (`make(chan T)`) that both *moves* a value and *synchronizes* the two goroutines. An unbuffered send blocks until a receiver is ready.
3. **close + range** - the sender calls `close(ch)` to say "no more"; the receiver loops with `for v := range ch` until it's drained.
4. **`select`** - wait on several channels at once and act on whichever is ready first; pair with `time.After` for timeouts.
5. **`sync.WaitGroup`** - `Add`, `Done` (via `defer`), `Wait` to block until a batch of goroutines all finish.
6. **The mantra** - share memory by communicating: hand values down channels instead of locking shared variables. ⚠️ Watch for deadlock (send with no receiver) and leaks (a goroutine blocked forever).

You can now run work concurrently and coordinate it safely. Next: handling what goes *wrong* - in Go, errors are ordinary values you pass around, not exceptions thrown from the shadows.


---

# Errors & I/O - Errors Are Values

If you're coming from a language with exceptions, Go's error handling will feel either refreshing or relentless, and usually both. There's no `try`/`catch`, no invisible stack-unwinding to a handler three floors up:

> **An error is just a value.** A function that can fail returns one, right alongside its result, and you deal with it then and there.

An error isn't a special control-flow event - it's data. The function hands back "here's the answer, and here's what went wrong (or `nil` if nothing did)." That's why `if err != nil` is everywhere in Go: the language insists you look at the failure at the exact spot it happened, while you still have context to act.

## The `if err != nil` pattern

By convention, a Go function that can fail returns its result *and* an `error` as its last value. Check the error immediately; if it's not `nil`, something went wrong.

📝 **Terminology.** `error` is a built-in interface - anything with an `Error() string` method is an error. The zero value of an error is `nil`, meaning "no error." So `err != nil` literally reads as "an error is present."

`strconv.Atoi` turns a string into an int, and it can fail (the string might not be a number):
```go
package main

import (
	"fmt"
	"strconv"
)

func main() {
	n, err := strconv.Atoi("42")
	if err != nil {
		fmt.Println("could not parse:", err)
		return
	}
	fmt.Println("parsed:", n)
}
```
```console
$ go run main.go
parsed: 42
```
`Atoi` returned two values: the parsed number `42` and an error. We checked `err` right away; it was `nil`, so we trusted `n` and printed it. Passing `"oops"` instead would make `err` non-nil (`strconv.Atoi: parsing "oops": invalid syntax`), taking the error branch. Result and error come back *together*, and you decide what to do with the failure on the spot.

⚠️ **Gotcha - ignoring the error.** Go lets you discard a return value with `_`, and the single most common Go mistake is doing that to the error:
```go
n, _ := strconv.Atoi(userInput) // DON'T: if userInput is garbage, n is silently 0
```
You threw the error away. If `userInput` was `"abc"`, `Atoi` returned `0` for `n` *and* a non-nil error explaining why - but ignoring it means holding `0` as if the parse succeeded. The bug surfaces later, far from the cause, as a mysterious zero. The whole point of errors-as-values is that you *see* them; `_` throws that away. (The `errcheck` linter, part of `golangci-lint` in the next phase, catches exactly this.)

## Returning errors from your own functions

When your function can fail, follow the same convention: return `(result, error)`. Pass a downstream error back up, or create your own with `errors.New` or `fmt.Errorf`.

```go
package main

import (
	"errors"
	"fmt"
)

func half(n int) (int, error) {
	if n%2 != 0 {
		return 0, errors.New("number is odd")
	}
	return n / 2, nil
}

func main() {
	for _, n := range []int{8, 7} {
		h, err := half(n)
		if err != nil {
			fmt.Printf("half(%d): %v\n", n, err)
			continue
		}
		fmt.Printf("half(%d) = %d\n", n, h)
	}
}
```
```console
$ go run main.go
half(8) = 4
half(7): number is odd
```
`half` returned `(result, nil)` on success and `(0, an error)` on failure. The caller checked `err` each time: for `8` it printed the result, for `7` the error. The failed case still returns a real `int` (`0`) - Go requires *all* return values, so convention is a zero/empty result alongside a non-nil error, and the caller knows not to trust the result when the error is set.

## Wrapping errors with `%w`

A bare "file not found" tells you *what* but not *where in your program* it happened. Wrapping adds your context while keeping the original error intact underneath, via `fmt.Errorf` and the special `%w` verb.

**Why this matters.** `%w` doesn't just stuff the old message into a new string - it *links* the new error to the original so tools can still dig it out later (the `errors.Is`/`errors.As` trick below). You get a readable chain: high-level context on the outside, root cause on the inside.

```go
package main

import (
	"errors"
	"fmt"
)

var errNotFound = errors.New("not found")

func loadUser(id int) error {
	return fmt.Errorf("loadUser(%d): %w", id, errNotFound)
}

func main() {
	err := loadUser(7)
	fmt.Println(err)
}
```
```console
$ go run main.go
loadUser(7): not found
```
`fmt.Errorf` built a new message - `loadUser(7): not found` - but `%w` also kept a hidden pointer to the original `errNotFound`. The message reads top-down (context first, root cause last), and the original error is still recoverable. Use `%w` when callers might need the cause; use plain `%v` (just formats the text) when you only need it readable.

## Inspecting errors: `errors.Is` and `errors.As`

Wrapping forms a chain, so you often need to ask: *"is this (anywhere in the chain) a specific known error?"* and *"is this a specific error type, and can I get at its fields?"* That's `errors.Is` and `errors.As`.

- **`errors.Is(err, target)`** - true if `err`, or anything it wraps, *is* that sentinel value. Use it to compare against a known error like `errNotFound` or `os.ErrNotExist`. (Don't use `==` - that only checks the outermost error and misses wrapped ones.)
- **`errors.As(err, &target)`** - true if `err`, or anything it wraps, is of a given *type*; if so it fills `target` so you can read the type's fields.

```go
package main

import (
	"errors"
	"fmt"
)

var errNotFound = errors.New("not found")

func loadUser(id int) error {
	return fmt.Errorf("loadUser(%d): %w", id, errNotFound)
}

func main() {
	err := loadUser(7)
	if errors.Is(err, errNotFound) {
		fmt.Println("yes, this was a not-found error")
	}
}
```
```console
$ go run main.go
yes, this was a not-found error
```
Even though `err`'s text was the wrapped `loadUser(7): not found`, `errors.Is` walked the chain, found `errNotFound` underneath, and matched it. A plain `err == errNotFound` would return false - the outer wrapper isn't equal to the sentinel - exactly why `errors.Is` exists. Reach for `errors.As` (a pointer to a variable of the error type) when you need the structured data inside, not just "is it this kind."

## Reading files: `os` and `bufio`

File and stream handling lives mostly in `os` and `bufio`, and every operation returns an error you check. For a whole small file, `os.ReadFile` gives you the bytes in one call. For reading line by line without loading it all into memory, `bufio.Scanner` is the standard tool.

```go
package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	f, err := os.Open("notes.txt")
	if err != nil {
		fmt.Println("open failed:", err)
		return
	}
	defer f.Close() // always runs, even on early return

	scanner := bufio.NewScanner(f)
	for scanner.Scan() { // advances to the next line; false at EOF
		fmt.Println("line:", scanner.Text())
	}
	if err := scanner.Err(); err != nil { // check for a read error
		fmt.Println("scan failed:", err)
	}
}
```
```console
$ cat notes.txt
buy milk
call dentist
$ go run main.go
line: buy milk
line: call dentist
```
`os.Open` returned the open file and an error; we checked it, then set up `defer f.Close()` so the file closes no matter how the function exits (an idiom covered in [Phase 9](09-idioms-and-gotchas.md)). `bufio.NewScanner` wrapped the file; `scanner.Scan()` returned `true` per line and `false` at end-of-file, and `scanner.Text()` gave the line's contents. After the loop we called `scanner.Err()` - `Scan()` returns `false` both for a clean EOF *and* a read error, and only `scanner.Err()` tells the two apart.

⚠️ **Gotcha.** `scanner.Scan()` returning `false` does **not** mean "success." It means "stop looping" - a normal EOF *or* a real I/O error. Always check `scanner.Err()` after the loop, or a disk error mid-read will look exactly like a clean finish.

## `panic` and `recover` - the rare exception

`panic` *is* Go's exception-like mechanism: it stops normal flow, unwinds the stack running deferred functions, and crashes the program with a stack trace. `recover` (only meaningful inside a `defer`) can catch a panic and stop the unwind. Go wants you to almost never use them for ordinary errors.

**When it's appropriate.** Use `panic` for *truly unrecoverable* programmer errors - an impossible state, a violated invariant, a config too broken to start. Use ordinary error values for normal, expected failures (file missing, bad input, network down). The dividing line: "is this a bug, or a Tuesday?" Bugs may panic; Tuesdays return an error.

```go
package main

import "fmt"

func safeDivide(a, b int) (result int, err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("recovered from: %v", r)
		}
	}()
	return a / b, nil // dividing by zero panics
}

func main() {
	_, err := safeDivide(10, 0)
	fmt.Println("err:", err)
}
```
```console
$ go run main.go
err: recovered from: runtime error: integer divide by zero
```
`10 / 0` triggered a runtime panic, which would normally crash the program. But the deferred function ran during the unwind, called `recover()` (returning the panic value instead of `nil`), and assigned a normal error to the named return value `err` - converting the panic back into an ordinary error the caller could check. This pattern - recover at a boundary, turn panic into error - is the main legitimate use of `recover`, reserved for guarding against bugs, not expected failures.

⚠️ **Gotcha.** Don't use `panic`/`recover` as cheap exceptions to dodge `if err != nil`. A panic that escapes a goroutine crashes the *entire* program - `recover` only works in the same goroutine that's unwinding. Errors-as-values is the path; panic is the emergency exit.

## Recap

1. **Errors are values** - a fallible function returns `(result, error)`; you check `if err != nil` right there, where you have the context to act.
2. **Return them** - pass downstream errors up; create your own with `errors.New` / `fmt.Errorf`. Return a zero result alongside a non-nil error.
3. **Wrap with `%w`** - `fmt.Errorf("doing X: %w", err)` adds your context while keeping the original error recoverable.
4. **Inspect with `errors.Is` / `errors.As`** - match a sentinel anywhere in the chain (`Is`), or pull out a specific error *type* and its fields (`As`). Don't use `==`.
5. **I/O** - `os.Open` + `bufio.Scanner` reads line by line; `defer f.Close()`; always check `scanner.Err()` after the loop.
6. **`panic`/`recover`** - Go's real exceptions, reserved for unrecoverable bugs, not everyday failures. ⚠️ Never ignore an error with `_`.

You now write code that fails clearly. Next: stepping back from the language to the *toolbox* around it - the batteries-included commands (`go build`, `go test`, `go fmt`) that make Go projects low-fuss to work in.


---

# The Ecosystem & Tooling - Batteries Included

Picking up a new language usually means a second, miserable project: choosing a build tool, a test runner, a formatter, a linter, a package manager - and making them all agree. Go's answer is one of its best features:

> **The toolbox comes in the box.** Install Go and you already have the build tool, the test runner, the formatter, the vetter, and the dependency manager - one command, `go`, with subcommands.

Each tool is a `go <verb>`, each does one job, and together they're why an unfamiliar Go project is usually trivial to build, test, and read.

## `go run` and `go build` - execute vs. produce

`go run` compiles your code to a temporary binary and runs it immediately - the fast feedback loop while working. `go build` compiles and *keeps* the result: a single, self-contained executable you can ship.

📝 **Terminology.** Go compiles to a single native binary with no separate runtime to install on the target machine - build it once and copy that one file. That's why "deploy a Go service" is often "scp one file," and why Docker images for Go are tiny.

```console
$ go run .
Hello, Missing Manual!

$ go build -o hello .
$ ls -lh hello
-rwxr-xr-x  1 you  staff   2.1M Jun 19 10:22 hello
$ ./hello
Hello, Missing Manual!
```
`go run .` compiled the package in the current directory and ran it on the spot, leaving no file behind - perfect for iterating. `go build -o hello .` did the compile but wrote the result to a file named `hello`, which we then ran directly. The binary is a couple of megabytes because Go statically bundles everything it needs - no dependencies to chase on the server.

## `go fmt` - the end of formatting debates

`go fmt` (which runs the `gofmt` tool) rewrites your code into Go's one true canonical format: tabs, brace placement, spacing, alignment - all non-negotiable. There's nothing to argue over because there are essentially no options.

**Why this is a feature, not a constraint.** Every other language community has burned years on tabs-vs-spaces, brace style, line length. Go ended the war by decree: one format, tool-enforced, so *all* Go code - yours, the standard library's, a stranger's on GitHub - looks the same. Code review stops being about style and starts being about substance. Most editors run `gofmt` on save.

```console
$ cat messy.go
package main
import "fmt"
func main(){
fmt.Println( "hi" )
}

$ gofmt -w messy.go
$ cat messy.go
package main

import "fmt"

func main() {
	fmt.Println("hi")
}
```
The original file had crooked indentation, a cramped `import`, and stray spaces inside the parentheses. `gofmt -w` (`-w` writes changes back to the file) rewrote it into the canonical layout - blank line after the package clause, tab indentation, tidy spacing. One correct answer, and the tool applied it.

💡 **Key point.** Don't hand-format Go and don't argue about style in review - let `gofmt` do it. "Run it through gofmt" is the entire formatting policy of the Go world.

## `go vet` - catches suspicious code the compiler allows

`go vet` is a built-in static analyzer that flags code that compiles fine but is *probably* a bug - the classic example being a `Printf` whose format verbs don't match its arguments.

```console
$ cat main.go
package main

import "fmt"

func main() {
	name := "Ada"
	fmt.Printf("hello %d\n", name) // %d expects an int, but name is a string
}

$ go vet .
# example/hello
./main.go:7:2: fmt.Printf format %d has arg name of wrong type string
```
The code compiled - `Printf` takes any arguments - but it's wrong: `%d` is for integers and `name` is a string, so at runtime you'd get garbled output like `hello %!d(string=Ada)`. `go vet` spotted the mismatch *before* you ran it - the cheap safety net for bugs the compiler is too permissive to reject.

## `go test` - testing is built in

Go's test runner is part of the toolchain. Write tests in files named `*_test.go`, in functions named `TestXxx(t *testing.T)`, and run them with `go test`. No framework to install, no config file.

Given a function and a test beside it:
```go
// math.go
package mathx

func Double(n int) int { return n * 2 }
```
```go
// math_test.go
package mathx

import "testing"

func TestDouble(t *testing.T) {
	got := Double(3)
	if got != 6 {
		t.Errorf("Double(3) = %d; want 6", got)
	}
}
```
```console
$ go test ./...
ok  	example/mathx	0.003s
```
`go test ./...` found every test in the module (`./...` means "this directory and all subdirectories"), compiled them with their packages, ran each `TestXxx`, and reported `ok` because nothing called `t.Errorf`. A test "fails" by reporting through `t` - no assert library, just a plain `if` and `t.Errorf` when reality doesn't match expectations.

📝 **Terminology.** `./...` is Go's wildcard for "the current directory and everything beneath it recursively." You'll use it constantly - `go test ./...`, `go vet ./...`, `go build ./...`.

## `go mod` - dependencies, the modern way

A *module* is the unit Go uses to track your project and its dependencies, defined by a `go.mod` file at the root. `go mod init` creates it, `go get` adds a dependency, and `go mod tidy` syncs `go.mod` to exactly what your code actually imports.

```console
$ go mod init example/hello
go: creating new go.mod: module example/hello

$ go get github.com/google/uuid
go: added github.com/google/uuid v1.6.0

$ go mod tidy
$ cat go.mod
module example/hello

go 1.25

require github.com/google/uuid v1.6.0
```
`go mod init` created a `go.mod` declaring your module's name. `go get` downloaded the `uuid` package, recorded the exact version (`v1.6.0`), and wrote a `go.sum` file with cryptographic checksums so future downloads are verifiably identical. `go mod tidy` reconciled `go.mod` with your real imports - adding what you started using, removing what you stopped. (Project layout and `go.mod` in depth: [Phase 5](05-modules-and-project-layout.md); this is the tooling side.)

💡 **Key point.** `go mod tidy` is the one to run before you commit - it guarantees `go.mod`/`go.sum` exactly match your imports, so a fresh clone builds with no surprises.

## The standard library - more "batteries" than you expect

Go's standard library is unusually broad and production-grade. A real HTTP server, JSON encoding/decoding, cryptography, file and OS access, regular expressions, templating, and the testing tools above - all in the box, no third-party packages required.

An HTTP server in the standard library alone:
```go
package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello from net/http!")
	})
	http.ListenAndServe(":8080", nil)
}
```
```console
$ go run . &
$ curl localhost:8080
Hello from net/http!
```
With nothing but `net/http`, we registered a handler for `/` and started a real web server on port 8080 - no framework, no dependencies in `go.mod`. The `curl` hit it and got the response. This is why a lot of Go services run on the standard library for a long time before reaching for anything external. (Popular web frameworks built on top of this are signposted in [Phase 18](18-where-to-go-next.md).)

## `golangci-lint` - the one tool worth adding

The toolchain covers almost everything, but the community standard for deeper linting is `golangci-lint` - a fast runner bundling dozens of linters (including `go vet` and the `errcheck` "you ignored an error" check from [Phase 7](07-errors-and-io.md)) behind one command.

```console
$ golangci-lint run ./...
main.go:12:2: Error return value of `f.Close` is not checked (errcheck)
		f.Close()
		^
```
`golangci-lint run ./...` ran its whole battery of linters in one pass and flagged an unchecked error - exactly the silent-failure trap from the errors phase. It's the single external tool most Go teams install; everything else they need already shipped with Go. (Install from the official instructions at golangci-lint.run - versions and install methods change, so check the source rather than copying a command that may be stale.)

## Recap

1. **`go run` / `go build`** - run on the spot vs. produce a single self-contained binary you can ship.
2. **`go fmt`** - one canonical format, enforced; it ends style debates so review is about substance.
3. **`go vet`** - flags compiles-but-probably-wrong code (like `Printf` verb mismatches).
4. **`go test ./...`** - built-in test runner; tests are `TestXxx(t *testing.T)` in `*_test.go`, no framework needed.
5. **`go mod`** - `init`, `get`, and especially `tidy` to keep dependencies exactly matching your imports.
6. **Standard library + `golangci-lint`** - batteries like `net/http` and `encoding/json` are built in; `golangci-lint` is the one external tool most teams add.

You've got the language and the toolbox. What's left is a handful of conventions and gotchas that bite everyone once.


---

# Idioms & Common Gotchas

You can write working Go without knowing any of this. But the gap between "compiles and runs" and "looks like Go" is a handful of conventions - plus a short list of gotchas, mostly about how Go quietly *shares* memory, that bite essentially every developer once.

The idioms: Go prefers *small, composable pieces over big inheritance hierarchies* - small interfaces, structs glued together by embedding, functions that take a narrow interface and hand back a concrete thing. The gotchas: *Go does what it says, not what you assumed*, and the assumptions that trip people up are almost always about slices, loops, and `nil`.

## Interfaces: small, and satisfied implicitly

An interface is a list of method signatures - a description of behavior, not data. Go's twist: a type satisfies an interface *automatically*, just by having the right methods. No `implements` keyword. If it has the methods, it fits.

**Why this changes how you design.** Because satisfaction is implicit, interfaces in Go are usually *tiny* - often a single method - and frequently defined by the *consumer*, not the producer. The most famous is `io.Writer`:
```go
type Writer interface {
	Write(p []byte) (n int, err error)
}
```
Anything with a `Write([]byte) (int, error)` method *is* an `io.Writer` - a file, a network connection, an in-memory buffer, an HTTP response - none of them ever mentioning the interface.

```go
package main

import (
	"fmt"
	"os"
	"strings"
)

func greet(w fmt.Stringer) { // any type with a String() string method fits
	fmt.Println(w.String())
}

type user struct{ name string }

func (u user) String() string { return "user: " + u.name }

func main() {
	greet(user{name: "Ada"}) // user satisfies fmt.Stringer just by having String()

	// And io.Writer in action: the same Fprintln targets a file or a buffer.
	var sb strings.Builder
	fmt.Fprintln(os.Stdout, "to the terminal")
	fmt.Fprintln(&sb, "to a buffer")
	fmt.Print(sb.String())
}
```
```console
$ go run main.go
user: Ada
to the terminal
to a buffer
```
`user` became a `fmt.Stringer` purely by having a `String()` method - we never declared the relationship. `fmt.Fprintln` wrote to two completely different destinations (the terminal and an in-memory `strings.Builder`) because both satisfy `io.Writer`. Small interfaces plus implicit satisfaction is why Go code composes so freely: you write to "anything that can be written to," and the caller picks what that is.

## Struct embedding: composition over inheritance

Go has no class inheritance. Instead, you *embed* one struct inside another by declaring it without a field name, and the outer struct gets the inner one's fields and methods promoted as its own - composition that *reads* like inheritance, without the hierarchy.

```go
package main

import "fmt"

type Engine struct{ Horsepower int }

func (e Engine) Start() { fmt.Println("vroom") }

type Car struct {
	Engine // embedded - no field name
	Brand  string
}

func main() {
	c := Car{Engine: Engine{Horsepower: 200}, Brand: "Tesla"}
	c.Start()                  // promoted from Engine
	fmt.Println(c.Horsepower)  // promoted field, no c.Engine.Horsepower needed
}
```
```console
$ go run main.go
vroom
200
```
`Car` embedded `Engine`, so `c.Start()` and `c.Horsepower` worked directly - the `Engine`'s method and field were *promoted* onto `Car`. There's no inheritance here: `Car` *has an* `Engine` and borrows its surface, composing pieces rather than building a tower of base classes.

## "Accept interfaces, return structs"

A widely-followed guideline: make function *parameters* interfaces (callers can pass anything that fits), but make *return values* concrete structs (callers get the real thing with all its methods).

**Why it works.** Accepting an interface keeps your function flexible - `func Save(w io.Writer, ...)` accepts a file, a buffer, a socket, anything writable. Returning a concrete struct keeps your *output* useful - the caller gets the full type and you're free to add methods later without breaking an interface contract. "Be liberal in what you accept, conservative in what you return," applied to types.

💡 **Key point.** When in doubt: parameters should be as *small and general* as the function needs (often a one-method interface); returns should be the *concrete* struct. Don't define an interface "just in case" - define it where it's *consumed*.

## `defer`: cleanup that can't be forgotten

`defer` schedules a function call to run when the *surrounding function* returns - no matter how (normal path, early `return`, or a panic) - guaranteeing cleanup sits right next to the thing that needs cleaning up.

**Why it's the idiom for cleanup.** You open something, then immediately `defer` closing it. The two lines live together, so you can't open-without-closing, or forget the close at the bottom of a long function with five early returns. Deferred calls run *last-in, first-out*, right for nested resources (close the inner thing before the outer).

```go
package main

import "fmt"

func main() {
	defer fmt.Println("3. cleanup runs last")
	fmt.Println("1. start")
	fmt.Println("2. work")
}
```
```console
$ go run main.go
1. start
2. work
3. cleanup runs last
```
The `defer` was written *first* but ran *last* - Go held that call until `main` was about to return. This is why `defer f.Close()` right after `os.Open` ([Phase 7](07-errors-and-io.md)) is bulletproof: the close fires on the way out, even with a `return` or panic in between. Pair every "open / lock / acquire" with an immediate `defer` of its "close / unlock / release."

## The gotcha cheat-card

Skim these now so they're familiar when they happen; the notes below explain the two sharpest.

| Gotcha | What bites you | The fix |
|---|---|---|
| **A nil interface is not nil** | Returning a typed-but-nil pointer as an `error` makes `err != nil` true even though "there's no error" | Return a literal `nil` for the error, never a nil-valued concrete type held in an interface |
| **Slice `append` aliasing** | `append` may return a slice that *shares* the original's backing array, so writing to one silently changes the other | If you need independence, `copy` into a fresh slice (or append to a `nil`/fresh slice) |
| **Range loop variable capture (pre-1.22)** | In Go ≤1.21, goroutines/closures in a `for` loop all captured the *same* loop variable, seeing only its final value | Go **1.22+** fixed this (each iteration gets a fresh variable); on older Go, copy `v := v` inside the loop |
| **Unused imports / variables won't compile** | A leftover import or an unused local is a *compile error*, not a warning | Delete it - or use the blank identifier `_` deliberately for an intentionally-unused import |
| **Exported = Capitalized** | A lowercase name (`doThing`, `count`) is package-private; only `DoThing`, `Count` are visible to other packages | Capitalize the first letter to export; this *is* the visibility rule, there's no `public` keyword |
| **Zero values, not "undefined"** | An uninitialized variable isn't null/garbage - it's the type's *zero value* (`0`, `""`, `false`, `nil` for pointers/slices/maps) | Lean on it (`var count int` is a usable `0`); but ⚠️ a `nil` map can be *read* but panics if you *write* to it - `make` it first |

⚠️ **The nil-interface trap, explained.** Genuinely confusing the first time - worth slowing down for:
```go
package main

import "fmt"

type myError struct{}

func (e *myError) Error() string { return "boom" }

func doThing() error {
	var p *myError = nil // a nil pointer...
	return p             // ...returned as an error interface
}

func main() {
	err := doThing()
	fmt.Println(err == nil) // false!  - surprising
}
```
```console
$ go run main.go
false
```
An interface value carries *two* things: a type and a value. `doThing` returned a `*myError` that was nil - but the interface holds the *type* `*myError` plus a nil value, and an interface is only equal to `nil` when *both* parts are empty. So `err == nil` is `false` even though the underlying pointer is nil, and the caller's `if err != nil` wrongly thinks there was an error. Fix: return a *bare* `nil` when there's no error - never a nil-valued concrete type stuffed into the interface.

⚠️ **Slice append aliasing, explained.** A slice is a small header (pointer, length, capacity) over a backing array. `append` reuses that array when there's spare capacity - so two slices can quietly point at the same memory:
```go
package main

import "fmt"

func main() {
	a := []int{1, 2, 3}
	b := a[:2]              // b shares a's backing array
	b = append(b, 99)      // capacity to spare → overwrites a[2] in place!
	fmt.Println(a)         // [1 2 99]  - a changed without you touching it
}
```
```console
$ go run main.go
[1 2 99]
```
`b := a[:2]` made `b` a window onto the same array as `a`, with room left over. `append(b, 99)` had spare capacity, so instead of allocating, it wrote `99` into the slot `a[2]` was using - mutating `a` as a side effect. When you need a slice you can grow without disturbing the original, copy the data into a fresh slice first.

📝 **Terminology.** A *zero value* is the default Go gives every variable you don't initialize - `0` for numbers, `""` for strings, `false` for bools, `nil` for pointers, slices, and maps. Go has no "uninitialized garbage" and no separate `null`; the zero value *is* the starting state, and good Go leans on it (an empty `sync.Mutex` is ready to use, a `nil` slice appends fine).

## Recap

1. **Interfaces are small and implicit** - satisfied automatically by having the methods; often one method, often defined by the consumer (`io.Writer`).
2. **Embedding, not inheritance** - compose structs by embedding; fields and methods get promoted.
3. **Accept interfaces, return structs** - flexible inputs, concrete outputs.
4. **`defer` for cleanup** - schedule the close/unlock right next to the open/lock; it runs on every exit path, LIFO.
5. **The cheat-card** - nil interface ≠ nil, slice append aliasing, range-var capture (fixed in 1.22), unused imports/vars are errors, exported = Capitalized, and zero values are real defaults (but don't write to a nil map).

That's idiomatic Go. You can now read other people's Go and write code that looks like it belongs. Next: where Go actually shines, what to build next, and where to go from here.


---

# Interfaces in Depth - Behavior, Not Hierarchy

In [Phase 9](09-idioms-and-gotchas.md) you met interfaces on the surface: small, implicitly satisfied lists of methods, like `io.Writer`. Underneath, though, is what an interface value actually *is* in memory - and once you see it, four separate-feeling quirks (type assertions, type switches, the empty interface, and the "my nil error isn't nil" bug) turn out to be one idea wearing four hats.

The mental model: **an interface value is not the thing you stored in it. It's a tiny box holding two slots - what type the thing is, and the thing itself.**

## The one idea: an interface value is a (type, value) pair

📝 **Interface value** - at runtime, a value of an interface type is a pair: a *dynamic type* (which concrete type is in the box right now) and a *dynamic value* (the actual data, or a pointer to it). The interface is just the box; the pair inside gives it behavior.

Assigning a concrete value to an interface variable makes Go record the concrete type alongside the value - the whole trick behind calling the *right* method later, and asking "what's actually in here?"

```mermaid
flowchart LR
  subgraph IF["var w io.Writer"]
    T["type:<br/>*os.File"]
    V["value:<br/>0xc0000a4..."]
  end
  C["os.Stdout<br/>(concrete)"] --> IF
  IF -->|"w.Write(...)"| M["calls<br/>(*os.File).Write"]
```

*What just happened:* assigning `os.Stdout` to an `io.Writer` variable filled both slots - type remembers `*os.File`, value points at the actual file. Calling `w.Write(...)` makes Go read the type slot to find *which* `Write` to run: **dynamic dispatch**, chosen at runtime from the concrete type in the box, not the interface type you declared. Two concrete types behind the same interface call two different methods - the entire point of an interface.

💡 **Why this matters.** Every feature in this phase is "do something with one of those two slots": assertions and switches *read the type slot*, the empty interface is *a box with no method requirements*, the nil trap is *the box being non-empty even when the value slot is nil*.

## Type assertions - getting the concrete value back out

A value inside an interface wears a disguise: you can only call the interface's methods, not the concrete type's other methods or fields. A **type assertion** pulls the disguise off - "I believe the concrete type here is `X`; give it back to me as an `X`."

📝 **Type assertion** - `x.(T)` checks interface value `x`'s dynamic type against `T`. Two forms: *comma-ok* `v, ok := x.(T)` (safe - `ok` tells you whether it matched), and *single-value* `v := x.(T)` (**panics** if the type doesn't match).

```go
package main

import "fmt"

func main() {
	var x any = "hello" // an interface box holding a string

	// comma-ok form: never panics, ok reports the match
	s, ok := x.(string)
	fmt.Println(s, ok)

	n, ok := x.(int) // wrong type - no panic, just ok == false
	fmt.Println(n, ok)

	// single-value form: panics if wrong. Use only when you're certain.
	s2 := x.(string)
	fmt.Println(s2)
}
```
```console
$ go run main.go
hello true
0 false
hello
```

*What just happened:* `x`'s type slot says `string`. `x.(string)` matched, so `s` got `"hello"` and `ok` was `true`; `x.(int)` didn't match, but comma-ok meant no panic - just `n = 0` and `ok = false`. The single-value `s2 := x.(string)` worked because we genuinely had a string; written as `x.(int)` it would have crashed with `panic: interface conversion: interface {} is string, not int`.

⚠️ **Reach for comma-ok by default.** Single-value is fine when a panic is the correct response to a broken assumption, but ordinary code almost always wants `ok` so it can handle "not that type" gracefully instead of taking down the program.

## Type switches - branching on the dynamic type

Chaining assertions for *several* possible concrete types gets ugly fast. The **type switch** reads the type slot and branches, binding the unwrapped value in each case.

📝 **Type switch** - `switch v := x.(type) { case T1: ... case T2: ... }`. The `.(type)` syntax is legal only inside a `switch`. In each `case`, `v` is already converted to that case's type, so you can use it directly.

Here's a small formatter that renders different types in different ways:

```go
package main

import "fmt"

type Point struct{ X, Y int }

func describe(x any) string {
	switch v := x.(type) {
	case int:
		return fmt.Sprintf("an int, doubled: %d", v*2)
	case string:
		return fmt.Sprintf("a string of length %d", len(v))
	case Point:
		return fmt.Sprintf("a point at (%d, %d)", v.X, v.Y)
	case nil:
		return "nothing at all"
	default:
		return fmt.Sprintf("some other type: %T", v)
	}
}

func main() {
	fmt.Println(describe(21))
	fmt.Println(describe("go"))
	fmt.Println(describe(Point{3, 4}))
	fmt.Println(describe(nil))
	fmt.Println(describe(3.14))
}
```
```console
$ go run main.go
an int, doubled: 42
a string of length 2
a point at (3, 4)
nothing at all
some other type: float64
```

*What just happened:* each `case` matched against `x`'s dynamic type - in `int`, `v` was already usable (`v*2` compiled); in `Point`, `v` had real `.X`/`.Y` fields. `nil` caught the empty box, and `default` swept up `float64` (`%T` prints the dynamic type). One switch replaced five separate comma-ok assertions.

## The empty interface and `any` - holds anything, knows nothing

An interface requiring *zero* methods accepts everything, since every type trivially has "no methods at all." That's the **empty interface**, written `interface{}`; since Go 1.18 it has a readable, identical alias: **`any`**.

📝 **`any` / `interface{}`** - an interface with no methods, so every value satisfies it: the universal box. You can put anything in, but call *no* methods on it until you pull the concrete type back out (with an assertion or type switch).

**When it's the right tool.** Genuinely heterogeneous data where you can't know the types ahead of time: decoded JSON (`map[string]any`), `fmt.Println`'s variadic `...any`, a cache storing arbitrary values.

```go
package main

import "fmt"

func main() {
	// A bag of mixed types - the empty interface earns its keep here.
	row := []any{"Ada", 36, true, 3.14}
	for _, field := range row {
		fmt.Printf("%v (%T)\n", field, field)
	}
}
```
```console
$ go run main.go
Ada (string)
36 (int)
true (bool)
3.14 (float64)
```

*What just happened:* `[]any` held a string, an int, a bool, and a float in one slice - impossible with a typed slice. Each element kept its own (type, value) pair, which is why `%T` reported the real type of each - the legitimate use case: data that's *inherently* mixed.

💡 **Prefer a specific interface whenever you can name the behavior you need.** `any` discards type safety - the compiler can't catch a wrong-type mistake, and every use site must assert the type back out. If what you need is "something I can write to" or "something with a `String()` method," declare *that* interface (`io.Writer`, `fmt.Stringer`). Code smell: a function taking `any` and immediately type-switching on known types usually wanted those types as a small interface instead.

## The nil-interface trap, fully explained

Phase 9 named this one and showed the symptom; now you have the model for *why* - one of the most baffling bugs in Go, and it follows directly from the (type, value) pair.

⚠️ **An interface is `nil` only when *both* slots are empty - type *and* value.** If the type slot holds a concrete type, the interface is **not nil**, even with a nil pointer in the value slot. A nil *pointer* and a nil *interface* are not the same thing.

The classic bug: a function returns a `*MyError`, and a nil one accidentally becomes a non-nil `error`.

```go
package main

import "fmt"

type MyError struct{ msg string }

func (e *MyError) Error() string { return e.msg }

// BUG: returns the concrete pointer type, even when there's no error.
func doWork(fail bool) error {
	var e *MyError // nil pointer of type *MyError
	if fail {
		e = &MyError{msg: "it broke"}
	}
	return e // <- always wraps *MyError into the error box
}

func main() {
	err := doWork(false) // we expect "no error"
	if err != nil {
		fmt.Println("caller thinks there was an error:", err)
	} else {
		fmt.Println("no error")
	}
}
```
```console
$ go run main.go
caller thinks there was an error: <nil>
```

*What just happened:* even with `fail == false`, `e` is a nil `*MyError` - but `return e` stuffs it into the `error` interface, filling the **type slot** with `*MyError`. The value slot is nil, the type slot isn't, so `err != nil` is `true` and the caller wrongly concludes work failed. (When printing, `fmt` calls `.Error()`, which panics dereferencing the nil `e` - but `fmt` recovers from that panic and, seeing a nil pointer, prints `<nil>`. Either way, the `if` already took the wrong branch.)

The fix: never let a typed nil leak into the interface. Return a *bare* `nil` when there's no error:

```go
package main

import "fmt"

type MyError struct{ msg string }

func (e *MyError) Error() string { return e.msg }

// FIXED: return the *interface* nil explicitly when nothing went wrong.
func doWork(fail bool) error {
	if fail {
		return &MyError{msg: "it broke"} // non-nil error, correctly
	}
	return nil // both slots empty -> a true nil error
}

func main() {
	err := doWork(false)
	if err != nil {
		fmt.Println("error:", err)
	} else {
		fmt.Println("no error")
	}
}
```
```console
$ go run main.go
no error
```

*What just happened:* returning the literal `nil` on the happy path hands back an interface with *both* slots empty - a genuine nil `error`, so `err != nil` is `false` and the caller takes the right branch. The rule: don't declare a typed pointer variable and return it as an interface; return concrete errors only when you have one, a bare `nil` otherwise - why idiomatic Go writes `return nil` rather than `return someNilPointer`.

## Recap

1. **An interface value is a (type, value) pair.** The type slot remembers the concrete type; the value slot holds the data. This single fact explains everything else, including dynamic dispatch - the method that runs is chosen from the concrete type in the box.
2. **Type assertions read the type slot.** `v, ok := x.(T)` is the safe comma-ok form; `v := x.(T)` panics on a mismatch. Default to comma-ok unless a panic is the right answer.
3. **A type switch** (`switch v := x.(type)`) branches on the dynamic type and hands you the unwrapped value per case - the clean way to handle several possible types.
4. **`any` (alias for `interface{}`)** accepts every value because it requires no methods. Right for genuinely heterogeneous data (JSON, mixed bags); a smell when a specific named interface would say what you actually need.
5. ⚠️ **The nil trap:** an interface is nil only when *both* slots are empty. A nil `*MyError` returned as `error` fills the type slot, so `err != nil` is true. Return a bare `nil`, never a typed nil pointer.

You now understand interfaces as a small box that pairs behavior with data, not a hierarchy. Next: **generics**, Go's way to write one function or type that works across many concrete types *with* full compile-time type safety.

## Quick check

Test yourself on the idea that ties this whole phase together - the (type, value) pair:

```quiz
[
  {
    "q": "What is an interface value actually made of at runtime?",
    "choices": [
      "A pair: the dynamic type of what's stored, plus the value (or a pointer to it)",
      "Just the concrete value, with the type discarded once stored",
      "A copy of the interface's method list and nothing else",
      "A string naming the interface type, like \"io.Writer\""
    ],
    "answer": 0,
    "explain": "An interface value holds two slots - a dynamic type and a dynamic value. That pairing is what enables dynamic dispatch and type assertions, and it's the reason the nil-interface trap exists."
  },
  {
    "q": "You write `n := x.(int)` (single-value form) but `x` actually holds a string. What happens?",
    "choices": [
      "The program panics with an interface conversion error",
      "`n` is set to 0 and execution continues",
      "It's a compile error - the types don't match",
      "`n` becomes the string converted to an int"
    ],
    "answer": 0,
    "explain": "The single-value assertion panics on a type mismatch. To check safely without panicking, use the comma-ok form `n, ok := x.(int)`, which sets `ok` to false instead of crashing."
  },
  {
    "q": "A function does `var e *MyError; return e` as an `error`. Why is the returned error not nil?",
    "choices": [
      "The interface's type slot is filled with *MyError, so only the value slot is nil - and an interface is nil only when both slots are empty",
      "Go automatically converts nil pointers into non-nil errors for safety",
      "Because *MyError has an Error() method, which can never be nil",
      "It actually is nil; the comparison `err != nil` is buggy in Go"
    ],
    "answer": 0,
    "explain": "Returning a typed nil pointer fills the interface's type slot with *MyError. An interface equals nil only when both the type and value slots are empty, so this one is non-nil. The fix is to return a bare `nil` when there's no error."
  }
]
```


---

# Generics & Advanced Types - One Function, Many Types

For most of Go's life: write a perfectly good `Max` for `int`, then need the *same* logic for `float64`, then `string` - identical logic, three rewrites. That's the gap generics close.

The mental model: a **generic** function or type leaves a type *blank* and fills it in later. Write the logic once, parameterized over "some type `T`," and the compiler stamps out a correct, type-checked version for each concrete type you use - the reuse of `interface{}` with none of the safety loss. This phase walks from the pain to type parameters, constraints, generic types, and finishes with the method-set rule, the one "advanced types" piece that quietly breaks code if you don't know it.

## The problem generics solve

Before generics, there were exactly two ways to write "the same logic for many types," and both hurt.

**Option one: copy-paste per type.** Write `MaxInt`, `MaxFloat`, `MaxString` - identical bodies, three places for a bug to hide.

**Option two: `interface{}` (now spelled `any`) and type assertions** - one function, but you throw away the type information at the door and claw it back at runtime:

```go
package main

import "fmt"

func MaxAny(a, b any) any {
	// We've lost the types. Now we have to guess them back.
	ai := a.(int) // panics if a isn't actually an int
	bi := b.(int)
	if ai > bi {
		return ai
	}
	return bi
}

func main() {
	fmt.Println(MaxAny(3, 7))      // works
	fmt.Println(MaxAny("a", "b"))  // compiles fine, panics at runtime
}
```
```console
$ go run main.go
7
panic: interface conversion: interface {} is string, not int
```
*What just happened:* `MaxAny` compiled happily even though it can only handle `int` - `any` accepts *anything*, so the compiler can't warn you. The mistake surfaced at runtime as a panic, far from where you wrote it, and the `any` return type means callers must assert the result back too. Generics undo this trade: one function, but the compiler checks types *before* the program runs.

## Type parameters - leaving a type blank

📝 **Type parameter** - a named stand-in for a type (`T`, `K`, `V` by convention), written in `[...]` after the function or type name. It's filled in with a real type when the code is used, and the compiler type-checks each filled-in version - usually *inferring* it from the call.

Here's `Max`, written once, working for any ordered type. The `[T cmp.Ordered]` says "`T` is some type you can compare with `<` and `>`":

```go
package main

import (
	"cmp"
	"fmt"
)

func Max[T cmp.Ordered](a, b T) T {
	if a > b {
		return a
	}
	return b
}

func main() {
	fmt.Println(Max(3, 7))         // T inferred as int
	fmt.Println(Max(2.5, 1.5))     // T inferred as float64
	fmt.Println(Max("apple", "z")) // T inferred as string
}
```
```console
$ go run main.go
7
2.5
z
```
*What just happened:* One function, three types, zero runtime assertions. The compiler **inferred** `T` from each call and type-checked each separately - `Max(3, "z")` won't compile, because `int` and `string` aren't the same `T`. That compile-time rejection is the safety the `any` version threw away, handed back.

Generics shine for *container-shaped* helpers too - here's `Map`, transforming a slice of one type into another, with *two* type parameters:

```go
package main

import "fmt"

// Map turns a []T into a []U by applying f to each element.
func Map[T, U any](in []T, f func(T) U) []U {
	out := make([]U, len(in))
	for i, v := range in {
		out[i] = f(v)
	}
	return out
}

func main() {
	nums := []int{1, 2, 3}
	labels := Map(nums, func(n int) string {
		return fmt.Sprintf("#%d", n)
	})
	fmt.Println(labels)
}
```
```console
$ go run main.go
[#1 #2 #3]
```
*What just happened:* `Map[T, U any]` has an *input* element type `T` and a *different output* type `U`. The compiler inferred `T = int` from the slice and `U = string` from the function's return, giving a properly typed `[]string` - not `[]any` - with no cast needed. `any` here means "no constraint at all," fine since `Map` never touches the elements except to hand them to `f`.

## Constraints & type sets - what operations are allowed

The compiler only lets you use operations it can *prove* every possible `T` supports. A **constraint** is how you make that promise - it answers "what is this type allowed to do?" Two you'll use constantly:

- **`any`** - no constraint. The value can be passed around, stored, compared to `nil`, but not much else (you can't `+` it or `<` it). This is what `Map` used.
- **`comparable`** - the type supports `==` and `!=`. You need this for map keys and for "is this in the set?" checks.

For arithmetic or ordering, you need a constraint that permits those operators - that's where **type sets** come in: a constraint interface lists the concrete types it allows, and `~` means "this type *or* any type whose underlying type is this."

📝 **Type set** - the set of types a constraint permits, written as a list of types joined by `|` inside an interface. `~int` means "int or any named type defined as int" (like `type Celsius int`). The allowed operations are whatever *all* listed types share.

```go
package main

import "fmt"

// Number permits any integer or float, including named types like `type Age int`.
type Number interface {
	~int | ~int64 | ~float64
}

func Sum[T Number](nums []T) T {
	var total T // zero value of whatever T is
	for _, n := range nums {
		total += n // allowed: every type in the set supports +
	}
	return total
}

type Age int // underlying type is int, so ~int covers it

func main() {
	fmt.Println(Sum([]int{1, 2, 3}))
	fmt.Println(Sum([]float64{1.5, 2.5}))
	fmt.Println(Sum([]Age{30, 40})) // works because of the ~
}
```
```console
$ go run main.go
6
4
70
```
*What just happened:* `Number` defines a type set - `int`, `int64`, or `float64` - and `Sum` may use `+` *only because every type in that set supports it*. `~int` (rather than plain `int`) is what lets `Age`, a named type whose underlying type is `int`, slip through; without it, only the literal type `int` would qualify. A constraint is "which operations the compiler will let your generic code perform," not "which types fit."

## Generic types - a typed container, written once

Type parameters aren't only for functions - a **struct** can be generic too, letting you build a `Stack`, `Set`, or `Cache` that holds one specific type without resorting to `[]any`:

```go
package main

import "fmt"

type Stack[T any] struct {
	items []T
}

func (s *Stack[T]) Push(v T) {
	s.items = append(s.items, v)
}

func (s *Stack[T]) Pop() (T, bool) {
	var zero T
	if len(s.items) == 0 {
		return zero, false // nothing to pop
	}
	last := s.items[len(s.items)-1]
	s.items = s.items[:len(s.items)-1]
	return last, true
}

func main() {
	var s Stack[string] // a stack that holds strings, period
	s.Push("a")
	s.Push("b")

	v, ok := s.Pop()
	fmt.Println(v, ok)

	_, ok = s.Pop() // drain it
	_, ok = s.Pop() // now empty
	fmt.Println("empty pop ok?", ok)
}
```
```console
$ go run main.go
b true
empty pop ok? false
```
*What just happened:* `Stack[T any]` is a generic struct; its methods carry `[T]` so they know which type they're operating on. Declaring `Stack[string]` locked `T` to `string` - `s.Push(42)` would be a compile error. The `var zero T` trick in `Pop` gives you a zero value (`""`, `0`, `nil`) for an unknown type, returned alongside `false` - the idiomatic way to signal "nothing there" without panicking.

## Method sets & receivers - the rule that bites

Now the "advanced types" piece that surprises people, and it's *not* about generics - it's about which methods count toward satisfying an interface, depending on whether a method has a **value receiver** or a **pointer receiver**.

📝 **Method set** - the set of methods a type "has" for the purpose of satisfying interfaces. For a value type `T`, the method set is only its *value-receiver* methods. For a pointer `*T`, the method set is *both* its value-receiver and pointer-receiver methods.

The rule that catches everyone: **if a method has a pointer receiver, only `*T` satisfies the interface - not `T`.**

```go
package main

import "fmt"

type Speaker interface {
	Speak() string
}

type Dog struct{ name string }

// Pointer receiver - only *Dog will satisfy Speaker.
func (d *Dog) Speak() string { return d.name + " says woof" }

func main() {
	var s Speaker

	s = &Dog{name: "Rex"} // *Dog has Speak() → fits
	fmt.Println(s.Speak())

	// s = Dog{name: "Rex"}  // ← uncomment: COMPILE ERROR
	// Dog (value) does NOT satisfy Speaker, because Speak has a *pointer* receiver.
}
```
```console
$ go run main.go
Rex says woof
```
*What just happened:* `Speak` has a pointer receiver `(d *Dog)`, so only `*Dog` is in the method set satisfying `Speaker`. A plain `Dog` value fails to compile with `Dog does not implement Speaker (method Speak has pointer receiver)`. Why the asymmetry? Go can take the address of an addressable value to call a pointer method, but can't guarantee an arbitrary interface-held value is addressable - so it refuses at the safe boundary.

⚠️ **Gotcha - pointer receiver, value passed.** The canonical Go interface bug: you define methods with pointer receivers (correct if they mutate state or the struct is large), then pass the *value* into something expecting the interface - a `[]Speaker`, a function parameter, a `json.Marshaler` slot - and get a confusing "does not implement" error. The fix is almost always `&thing` instead of `thing`. Reverse direction is fine: value-receiver-only methods satisfy the interface as both `T` and `*T`.

💡 **Key point - generics or an interface?** Use **generics** for *the same logic over many types* - `Max`, `Map`, `Stack`: the body never changes, only the type does. Use an **interface** for *different logic behind shared behavior* - a `Writer` that's a file vs. buffer vs. socket, each `Write` doing something genuinely different. Same code, varying type → generic; varying code, same call → interface.

```mermaid
flowchart TD
  A[Same operation, many types?] -->|Same body, type varies| B[Use generics]
  A -->|Different body, shared method| C[Use an interface]
```

## Recap

1. **Generics solve duplication without losing safety** - before them you copy-pasted per type or used `any` + assertions (which moved errors to runtime). Type parameters keep one function and keep compile-time checking.
2. **Type parameters** go in `[...]` after the name (`func Max[T cmp.Ordered](...)`, `Map[T, U any]`); the compiler usually *infers* the concrete type from the call.
3. **Constraints define allowed operations.** `any` = no operations beyond passing around; `comparable` = `==`/`!=`; custom **type sets** (`~int | ~float64`) permit only the operations all listed types share. The `~` admits named types with that underlying type.
4. **Generic types** like `Stack[T]` give you typed containers written once; use `var zero T` to produce a default value for an unknown type.
5. ⚠️ **Method sets:** a pointer-receiver method means *only* `*T` satisfies the interface, not `T`. Pass `&thing`, not `thing`, into interface slots when methods have pointer receivers.
6. 💡 **Generics vs. interfaces:** same logic over many types → generics; different logic behind a shared method → interface.

You can now write code reused across types without giving up the compiler's help, and you know the method-set rule that otherwise turns a one-character fix into an hour of confusion. Next: goroutines and channels, pulled together into real concurrency patterns.

## Quick check

Test yourself on the two ideas most likely to bite - constraints and method sets:

```quiz
[
  {
    "q": "Why does the `any` version of `Max` compile even though it only handles `int`, while the generic version catches the mistake?",
    "choices": [
      "`any` accepts any value, so the compiler can't check the types - the error only appears at runtime as a panic",
      "`any` is slower, so the compiler skips type checking to save time",
      "The generic version also fails at runtime; there's no real difference",
      "`any` automatically converts strings to ints before comparing"
    ],
    "answer": 0,
    "explain": "An `any` parameter accepts everything, so the compiler has no type information to verify against - bad calls compile and panic at runtime. A type parameter ties the arguments to one concrete `T` the compiler checks before the program runs."
  },
  {
    "q": "In the constraint `~int | ~float64`, what does the `~` add?",
    "choices": [
      "It admits named types whose underlying type is int or float64 (like `type Age int`), not just the literal types",
      "It makes the constraint match all numeric types automatically",
      "It marks the types as optional, so any type at all is allowed",
      "It enables approximate (floating-point) comparison"
    ],
    "answer": 0,
    "explain": "`~int` means 'int, or any named type whose underlying type is int.' Without the `~`, only the literal type `int` qualifies and a `type Age int` would be rejected."
  },
  {
    "q": "`Speak()` has a pointer receiver `(d *Dog)`. Which satisfies the `Speaker` interface?",
    "choices": [
      "Only `*Dog` - a plain `Dog` value does not implement Speaker",
      "Only `Dog` - pointer receivers don't count toward interfaces",
      "Both `Dog` and `*Dog`, always",
      "Neither - interfaces require value receivers"
    ],
    "answer": 0,
    "explain": "A pointer-receiver method is only in the method set of `*T`, so only `*Dog` satisfies the interface. Pass `&Dog{...}`, not `Dog{...}`. (The reverse holds for value receivers: those satisfy via both `T` and `*T`.)"
  }
]
```


---

# Concurrency Patterns - From Goroutines to Real Systems

In [Phase 6](06-goroutines-and-channels.md) you learned the two primitives - `go` to start a concurrent task, channels to pass values safely. Those are the bricks; this phase is the architecture built from them over and over in real Go programs.

The mental model: production concurrency is rarely "start one goroutine and wait." It's "start a controlled number of goroutines, give them a way to *stop* when the work is no longer needed, spread work across them, collect results, and protect anything they share." Each section below is one piece of that - no new machinery, just `go`, channels, and `sync` arranged into shapes you'll reach for by name.

## `select` - wait on several channels at once

You met `select` briefly in Phase 6 for a timeout - time to make it a tool you reach for deliberately, the control structure that turns one-channel toys into real coordination.

📝 **`select`** - a `switch` whose cases are channel operations (sends or receives). It blocks until *one* case can proceed, then runs exactly that case; if several are ready at once, it picks one at random, so no single channel can starve the others.

**Non-blocking with `default`.** Add a `default` case and `select` stops blocking: if no channel is ready *right now*, it runs `default` and moves on - how you "peek" without committing to wait.

```go
package main

import "fmt"

func main() {
	jobs := make(chan int, 2)
	jobs <- 1 // buffer has room, so this doesn't block

	select {
	case j := <-jobs:
		fmt.Println("got a job:", j)
	default:
		fmt.Println("no job ready")
	}

	select {
	case j := <-jobs:
		fmt.Println("got a job:", j)
	default:
		fmt.Println("no job ready") // buffer is empty now
	}
}
```
```console
$ go run main.go
got a job: 1
no job ready
```
*What just happened:* The first `select` found a value waiting and took the receive case; the second found the channel empty and, thanks to `default`, didn't block - it reported "no job ready" and moved on. Without `default`, that second receive would block forever (`deadlock!`). `default` is your "try, but don't wait" switch.

**A timeout with `time.After`.** The other everyday shape: "wait for a result, but give up after a while." `time.After(d)` returns a channel delivering one value after duration `d` - put it in a `select` beside the thing you're really waiting on, and whichever fires first wins.

```go
package main

import (
	"fmt"
	"time"
)

func main() {
	result := make(chan string)
	go func() {
		time.Sleep(2 * time.Second) // slow work
		result <- "done"
	}()

	select {
	case r := <-result:
		fmt.Println("got:", r)
	case <-time.After(500 * time.Millisecond):
		fmt.Println("gave up waiting")
	}
}
```
```console
$ go run main.go
gave up waiting
```
*What just happened:* `select` watched two channels - `result` and the `time.After` timer. The work needed 2 seconds; the timer fired at half a second, so the timeout case won. The slow goroutine is still out there, though - it'll eventually try to send on `result` with nobody listening, and *leak*. That dangling goroutine is exactly what `context` solves next.

💡 **Key point.** `select` is the join point of Go concurrency: "receive a result *or* time out," "send *or* bail if the consumer is gone," "wait on work *or* a cancellation signal" - all the same shape, list the channels, let whichever is ready win.

## `context` - telling goroutines to stop

The timeout above stopped *us* from waiting, but did nothing to stop the *worker*. In a real server - thousands of requests, each spawning goroutines - work nobody needs anymore has to actually stop, or you bleed memory and CPU. That's what `context` is for.

📝 **`context.Context`** - a value carrying a cancellation signal (plus a deadline and request-scoped values) down a call tree, via one channel, `ctx.Done()`, that *closes* when work should stop. Goroutines `select` on `ctx.Done()` and exit when it fires; cancel a context and every context derived from it is cancelled too.

Create one from a parent (`context.Background()`, or a server's request context) with a helper that also gives you a way to trigger cancellation:

- `context.WithCancel(parent)` - returns a `ctx` and a `cancel()` function you call to stop it.
- `context.WithTimeout(parent, d)` - auto-cancels after duration `d` (and still gives you a `cancel` to call early).

**A real example.** A worker that loops until it's told to stop:

```go
package main

import (
	"context"
	"fmt"
	"time"
)

func worker(ctx context.Context) {
	for {
		select {
		case <-ctx.Done(): // cancellation signal arrived
			fmt.Println("worker stopping:", ctx.Err())
			return
		default:
			fmt.Println("working...")
			time.Sleep(200 * time.Millisecond)
		}
	}
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
	defer cancel() // always release the context's resources

	worker(ctx)
}
```
```console
$ go run main.go
working...
working...
working...
worker stopping: context deadline exceeded
```
*What just happened:* The worker looped, doing a little work each pass. After 500ms the timeout fired, closing `ctx.Done()`; the `select` picked that case up, the worker printed why it stopped (`ctx.Err()` reports `context deadline exceeded`) and returned cleanly - no leak. The `defer cancel()` is non-negotiable: it frees the timer even when the timeout already fired, and `go vet` warns if you forget it.

💡 **Key point.** By strong convention, `ctx` is the **first parameter** of any function doing cancellable work: `func Fetch(ctx context.Context, url string) (...)`. Pass it down the call chain; never store it in a struct. When a request is cancelled or times out at the top, that signal propagates all the way down and every goroutine watching `ctx.Done()` unwinds - *the* idiom for not leaking goroutines.

## Worker pool - N goroutines sharing a queue of jobs

An unbounded number of goroutines (one per job, for a million jobs) will happily exhaust memory or hammer a database into the ground. The fix is a **worker pool**: a fixed crew of goroutines pulling jobs off one channel and pushing results onto another - parallelism *and* a cap on how much runs at once.

```mermaid
flowchart LR
  J[jobs chan] --> W1[worker 1]
  J --> W2[worker 2]
  J --> W3[worker 3]
  W1 --> R[results chan]
  W2 --> R
  W3 --> R
```

**A real example.** Three workers squaring numbers:

```go
package main

import (
	"fmt"
	"sync"
)

func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
	defer wg.Done()
	for n := range jobs { // pull jobs until the channel is closed and drained
		results <- n * n
	}
}

func main() {
	jobs := make(chan int, 100)
	results := make(chan int, 100)

	var wg sync.WaitGroup
	for w := 1; w <= 3; w++ {
		wg.Add(1)
		go worker(w, jobs, results, &wg)
	}

	for n := 1; n <= 6; n++ {
		jobs <- n
	}
	close(jobs) // no more jobs; workers' range loops will end

	wg.Wait()      // wait for all workers to finish
	close(results) // safe to close now: nobody is still sending

	sum := 0
	for r := range results {
		sum += r
	}
	fmt.Println("sum of squares:", sum)
}
```
```console
$ go run main.go
sum of squares: 91
```
*What just happened:* Three workers each ran `for n := range jobs`, competing to pull from the same channel - free load balancing. `close(jobs)` after sending all six jobs let each worker's `range` loop end once the channel drained. A `WaitGroup` tracked the workers; once `wg.Wait()` returned, every sender on `results` was done, so it was safe to `close(results)` and range over it. Result order is non-deterministic, but the **sum is deterministic** - 1+4+9+16+25+36 = 91.

⚠️ **Gotcha - close in the right order.** `close(results)` must come *after* `wg.Wait()` - closing while a worker still sends panics with "send on closed channel." The discipline: senders finish, then whoever knows they're all done closes the channel.

Notice the directional channel types: `jobs <-chan int` (receive-only) and `results chan<- int` (send-only). The compiler now *enforces* that a worker can only take from `jobs` and put onto `results` - a safety rail documenting the data flow.

## Fan-out / fan-in - split work, then merge results

A worker pool *is* fan-out/fan-in, named by its two halves:

- **Fan-out** - multiple goroutines reading from the *same* channel, dividing the work (our three workers ranging over `jobs`).
- **Fan-in** - multiple goroutines writing to the *same* channel, merging output into one stream (our three workers sending to `results`).

The new wrinkle in a standalone merge: when several producers feed one output channel, *who closes it?* Same tool as above - a `WaitGroup` counting the producers, plus one goroutine that waits then closes:

```go
func merge(cs ...<-chan int) <-chan int {
	out := make(chan int)
	var wg sync.WaitGroup
	wg.Add(len(cs))
	for _, c := range cs {
		go func(c <-chan int) {
			defer wg.Done()
			for v := range c {
				out <- v
			}
		}(c)
	}
	go func() {
		wg.Wait()  // all producers drained
		close(out) // ...so it's safe to close the merged channel
	}()
	return out
}
```
*What just happened:* `merge` starts one goroutine per input channel, each copying values into the shared `out` channel - the fan-in. A separate goroutine waits on the `WaitGroup` and closes `out` only once every producer has finished, so the caller writes a plain `for v := range merge(...)`. This "WaitGroup + closer goroutine" combo is the canonical way to safely close a channel with many writers.

## The `sync` toolbox - when to share memory instead

Channels are the headline, but aren't always the right tool. Sometimes you genuinely have *shared state* - a counter, a cache, a config loaded once - and a channel is more ceremony than it's worth. That's what `sync` is for.

**`sync.Mutex` - one writer at a time.** A mutex is a lock: `Lock()` before touching shared state, `Unlock()` after. Only one goroutine holds the lock at a time, so the protected section can't race.

```go
package main

import (
	"fmt"
	"sync"
)

type counter struct {
	mu sync.Mutex
	n  int
}

func (c *counter) inc() {
	c.mu.Lock()
	defer c.mu.Unlock() // unlock on every exit path
	c.n++
}

func main() {
	c := &counter{}
	var wg sync.WaitGroup
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			c.inc()
		}()
	}
	wg.Wait()
	fmt.Println("final count:", c.n)
}
```
```console
$ go run main.go
final count: 1000
```
*What just happened:* A thousand goroutines incremented the same `c.n`. `c.n++` looks atomic but isn't - read, add, write, three steps the scheduler can interrupt, so two goroutines can read the same value and lose an increment. The mutex serializes `inc`, so all 1000 land exactly. Remove the lock and you'd get something *less* than 1000, differently wrong each run. `defer c.mu.Unlock()` right after `Lock()` releases even if the body panics.

📝 **`sync.RWMutex`** - distinguishes readers from writers. Many goroutines can hold the *read* lock (`RLock`) at once, but a *write* lock (`Lock`) is exclusive. Reach for it when reads vastly outnumber writes (a config read on every request, written rarely) - readers run in parallel instead of queueing.

**`sync.WaitGroup` - recap.** Used throughout: `Add(n)` before launching, `defer Done()` inside each goroutine, `Wait()` to block until the counter hits zero.

**`sync.Once` - run something exactly once.** For one-time initialization (a config load, a singleton connection) several goroutines might trigger at once, `Once` guarantees the function runs a single time and every other caller blocks until it completes.

```go
var (
	once   sync.Once
	config map[string]string
)

func loadConfig() map[string]string {
	once.Do(func() {
		fmt.Println("loading config (this prints once)")
		config = map[string]string{"env": "prod"}
	})
	return config
}
```
*What just happened:* No matter how many goroutines call `loadConfig` simultaneously, `once.Do` runs its function exactly one time; the rest wait, then see the already-populated `config` - race-free lazy initialization without writing your own locking.

**So: channel or mutex?** The Phase 6 mantra - *"don't communicate by sharing memory; share memory by communicating"* - is the default, not a law:

- Use a **channel** when you're *passing ownership* of data between goroutines, or coordinating *who does what next*.
- Use a **mutex** when goroutines genuinely *share* one piece of state and each needs brief, exclusive access (a counter, an in-memory cache) - wrapping that in channels is usually more code for no benefit.

If unsure, start with a channel - it's harder to misuse. Drop to a mutex when the channel version feels like fighting the problem.

⚠️ **Gotcha - goroutine leaks, again.** The silent failure of every pattern here: a goroutine blocked forever on a send or receive that never happens never gets garbage-collected, and the runtime won't warn you. The slow worker from the `select`/timeout example was one. The fixes: give every long-lived goroutine a `ctx.Done()` case, and make sure every channel you send on has a guaranteed receiver (or gets closed). Leaks don't crash - they slowly eat memory until something far away falls over.

**And finally: the race detector.** Some concurrency bugs cannot be found by reading at all. A data race - two goroutines touching the same memory at once, at least one writing, no synchronization - might work fine for a million runs and corrupt data on the million-and-first. Go ships a tool that finds them for you:

```console
$ go run -race main.go     # run with the race detector on
$ go test -race ./...      # or, far better, run your whole test suite with it
```

Run with `-race` and Go instruments every memory access; the instant two goroutines touch the same location unsafely, it prints a `WARNING: DATA RACE` report naming the goroutines, variable, and stack traces - catching the lost-update bug from the mutex example, slice and map races, all of it. It's slower (don't ship a `-race` binary), but make `go test -race` part of your normal runs - the single highest-leverage habit in concurrent Go. If you write goroutines and never run it, you have bugs you haven't met yet.

## Recap

1. **`select`** waits on several channel operations and runs whichever is ready; add `default` for a non-blocking peek, and pair with `time.After` for timeouts. It's the join point of all the patterns here.
2. **`context`** carries a cancellation signal down a call tree. Goroutines `select` on `ctx.Done()` to stop cleanly; create one with `WithCancel`/`WithTimeout`, always `defer cancel()`, and pass `ctx` as the first parameter.
3. **Worker pool** caps concurrency: a fixed set of goroutines `range` over one jobs channel and send to one results channel. Close `jobs` to end the workers; `WaitGroup` then tells you when it's safe to close `results`.
4. **Fan-out / fan-in** is that pool by name - many readers split the work, many writers merge results. To close a many-writer channel safely, use a `WaitGroup` plus a goroutine that waits then closes.
5. **The `sync` toolbox** guards shared state directly: `Mutex`/`RWMutex` for exclusive (or read-shared) access, `WaitGroup` to wait for a batch, `Once` for one-time init. Channels to pass ownership; mutexes to share state. ⚠️ A goroutine blocked forever is a silent leak - give every one a way out.
6. **The race detector** (`go test -race`, `go run -race`) finds data races that reading the code never will. Make it part of your normal test runs.

You can now build the concurrent shapes real Go services are made of - bounded, cancellable, checked for races. Next: handling what goes *wrong*, the Go way.

## Quick check

Test yourself on the patterns that separate toy goroutines from production ones:

```quiz
[
  {
    "q": "In a worker pool, why must `close(results)` come after `wg.Wait()` rather than before?",
    "choices": [
      "Because a worker still sending on `results` would panic if the channel were already closed",
      "Because `close` only works on empty channels",
      "Because `wg.Wait()` is what allocates the results channel",
      "Because closing first would make the workers run twice"
    ],
    "answer": 0,
    "explain": "Sending on a closed channel panics. `wg.Wait()` returns only once every worker has finished (and therefore stopped sending), so it's the signal that closing `results` is now safe."
  },
  {
    "q": "What does a goroutine actually do to respond to a context being cancelled?",
    "choices": [
      "It `select`s on `ctx.Done()`, which becomes ready when the context is cancelled, and returns",
      "It polls `ctx.Cancelled` as a boolean every loop",
      "Go automatically kills the goroutine when `cancel()` is called",
      "It waits for the garbage collector to stop it"
    ],
    "answer": 0,
    "explain": "Cancellation closes the channel returned by `ctx.Done()`. A goroutine watches that channel in a `select`; once it's ready, the goroutine cleans up and returns on its own. Go never forcibly stops a goroutine - cooperation via `ctx.Done()` is the mechanism."
  },
  {
    "q": "You have a shared in-memory counter incremented by 1000 goroutines and the final total is sometimes less than 1000. What's the right fix?",
    "choices": [
      "Protect the increment with a `sync.Mutex` (or run with `-race` to confirm the data race first)",
      "Add a `time.Sleep` after each increment so they don't collide",
      "Run the program more times until it gives 1000",
      "Make the counter a global variable so all goroutines see it"
    ],
    "answer": 0,
    "explain": "`n++` is read-add-write, not atomic, so concurrent increments lose updates - a classic data race. A mutex serializes access so every increment lands. `go run -race` will pinpoint exactly this race; sleeping only hides it, and a global is still shared unsafely."
  }
]
```


---

# Error Handling, Deep - Wrapping, Inspecting & Recovering

Back in [Phase 7](07-errors-and-io.md) you learned the foundational truth of Go errors: an error is just a value. Return `(result, error)`, check `if err != nil`, deal with the failure right where it happened. That's enough to write clean, correct Go - but it's the *floor*, not the ceiling.

Here's the problem once programs get real. An error bubbles up through five function calls and lands in your logs as a bare `not found`. Not found *what*? By *whom*? At *which step*? The error has no memory of its own journey. This phase gives errors a memory, and your code the tools to interrogate it later.

The mental model: an error is a package that travels up the call stack. At each level, a function can *wrap* it in a new layer, writing its own note on the outside ("loading config: …") without discarding what's underneath, so by the top it carries the whole story. Wrapping writes it; `errors.Is` and `errors.As` read it back.

## Adding context by wrapping with `%w`

You met `%w` briefly in Phase 7 - now let's look at what it actually builds.

📝 **Wrapping** - enclosing an existing error inside a new one that adds context, while keeping a link back to the original. The result is an **error chain**: a stack of errors where each layer knows the one beneath it, built with `fmt.Errorf` using the `%w` ("wrap") verb.

The distinction that matters: `%v` *formats* an error into a string and forgets it; `%w` *links* the new error to the original so it can be recovered later. Same readable message, but only `%w` preserves the chain.

```go
package main

import (
	"errors"
	"fmt"
)

var ErrPermission = errors.New("permission denied")

func readSecret() error {
	return ErrPermission // the low-level failure
}

func loadConfig() error {
	if err := readSecret(); err != nil {
		return fmt.Errorf("loading config: %w", err) // wrap it
	}
	return nil
}

func startServer() error {
	if err := loadConfig(); err != nil {
		return fmt.Errorf("starting server: %w", err) // wrap again
	}
	return nil
}

func main() {
	err := startServer()
	fmt.Println(err)
}
```
```console
$ go run main.go
starting server: loading config: permission denied
```
*What just happened:* Each function added its own note with `%w` as the error travelled up, reading outside-in like a breadcrumb trail: top-level intent (`starting server`), sub-step (`loading config`), root cause (`permission denied`) - no manual string concatenation, each layer wrapped the one below. Crucially, the original `ErrPermission` is *still in there*, recoverable, not flattened into text - what makes the next section possible.

💡 **Key point.** A good wrap message describes what *this layer* was trying to do - `loading config`, `fetching user 42` - not a restatement of the error below it. Don't end it with a colon or the error itself; `%w` appends that for you.

## Inspecting wrapped errors: `errors.Is` and `errors.As`

A chain you can read with your eyes is nice; a chain your *code* can read is what makes wrapping powerful. Once wrapped, you can no longer ask "is this the not-found error?" with `==`, because you're holding the *outer* wrapper, not the original.

⚠️ **Gotcha - `==` only sees the outermost layer.** After `fmt.Errorf("loading config: %w", ErrPermission)`, the value you hold is a brand-new error whose identity is *not* `ErrPermission`, so `err == ErrPermission` is `false` even though `ErrPermission` sits one layer down. Comparing wrapped errors with `==` silently misses the match - the single most common wrapping bug.

The fix: two standard-library functions that walk the *entire* chain for you.

- **`errors.Is(err, target)`** - returns true if `err`, or anything it wraps, *is* the specific sentinel value `target`. Use it to answer "is this (somewhere) a known error?"
- **`errors.As(err, &target)`** - returns true if `err`, or anything it wraps, is of a specific *type*; if so, it fills in `target` so you can read that type's fields. Use it to answer "is this a known *kind* of error, and what's inside it?"

Here's `errors.Is` matching through the same three-layer chain from above:

```go
package main

import (
	"errors"
	"fmt"
)

var ErrPermission = errors.New("permission denied")

func startServer() error {
	return fmt.Errorf("starting server: %w",
		fmt.Errorf("loading config: %w", ErrPermission))
}

func main() {
	err := startServer()

	fmt.Println("== check:  ", err == ErrPermission)      // outer wrapper, not equal
	fmt.Println("Is check:  ", errors.Is(err, ErrPermission)) // walks the chain
}
```
```console
$ go run main.go
== check:   false
Is check:   true
```
*What just happened:* `err == ErrPermission` was `false` - the outermost wrapper, a different object entirely. But `errors.Is` peeled the chain layer by layer, found `ErrPermission` at the bottom, and matched it. That's the whole reason `errors.Is` exists: identity through wrapping. Reach for it any time you'd write `err == someKnownError`.

## Sentinel errors - a known value to match against

Both examples above lean on `var ErrPermission = errors.New(...)` - a **sentinel error**, worth naming as a pattern.

📝 **Sentinel error** - a package-level error value, declared once with `errors.New`, that callers compare against with `errors.Is` to recognize a specific, expected condition. The name conventionally starts with `Err`; the standard library is full of them: `io.EOF`, `sql.ErrNoRows`, `os.ErrNotExist`.

They shine when a failure is a single, well-known *condition* the caller will branch on - "the row wasn't found," "we hit end of file" - via `if errors.Is(err, sql.ErrNoRows)`.

```go
package main

import (
	"errors"
	"fmt"
)

var ErrNotFound = errors.New("user not found")

func findUser(id int) error {
	if id != 1 {
		return fmt.Errorf("findUser(%d): %w", id, ErrNotFound)
	}
	return nil
}

func main() {
	err := findUser(99)
	if errors.Is(err, ErrNotFound) {
		fmt.Println("handle gracefully: show a 404")
	} else if err != nil {
		fmt.Println("some other failure:", err)
	}
}
```
```console
$ go run main.go
handle gracefully: show a 404
```
*What just happened:* `findUser` wrapped the sentinel with call context, and the caller used `errors.Is` to recognize *exactly* the not-found case and respond (a 404) while letting other errors fall through. The sentinel is shared vocabulary: the producer publishes `ErrNotFound`, the consumer matches on it.

⚠️ **Gotcha - sentinels are an API promise.** Once you export `ErrNotFound`, every caller writing `errors.Is(err, ErrNotFound)` is *coupled* to it - you can't rename or remove it without breaking them, and you can't attach per-occurrence detail (which user? which id?) since it's one shared, immutable value. Sentinels suit plain yes/no conditions; when the caller needs *structured data*, you've outgrown them - what custom error types are for.

## Custom error types - errors that carry data

A sentinel says "this kind of thing went wrong." A custom error type says "here are the specifics" - an ordinary struct satisfying the `error` interface via an `Error() string` method, a real error you can return but with fields callers can pull out.

📝 **Custom error type** - a struct implementing `error` (an `Error() string` method), letting a single error value carry structured fields (a field name, a code, an offending value). Callers extract it from a chain with `errors.As`.

`errors.Is` checks identity against a value; `errors.As` checks for a *type* and hands you the value so you can read its fields.

```go
package main

import (
	"errors"
	"fmt"
)

// A struct that is also an error.
type ValidationError struct {
	Field string
	Msg   string
}

func (e *ValidationError) Error() string {
	return fmt.Sprintf("validation failed on %q: %s", e.Field, e.Msg)
}

func register(age int) error {
	if age < 0 {
		return fmt.Errorf("register: %w",
			&ValidationError{Field: "age", Msg: "must not be negative"})
	}
	return nil
}

func main() {
	err := register(-5)

	var ve *ValidationError
	if errors.As(err, &ve) { // pull the concrete type out of the chain
		fmt.Println("field that failed:", ve.Field)
		fmt.Println("full message:     ", err)
	}
}
```
```console
$ go run main.go
field that failed: age
full message:      register: validation failed on "age": must not be negative
```
*What just happened:* `ValidationError`'s `Error()` method makes it an `error`, so `register` wrapped and returned it like any other. `errors.As(err, &ve)` walked the chain, found a `*ValidationError` underneath, and copied it into `ve` - structured detail (`ve.Field` is `"age"`), not just a string to parse. The power split: `errors.Is` for "is it this specific error?", `errors.As` for "is it this *kind*, and give me its data."

💡 **Key point.** Note the pointer receiver and the `&` everywhere - return `&ValidationError{...}`, match with `var ve *ValidationError; errors.As(err, &ve)`. Using a pointer type consistently avoids a subtle mismatch where `errors.As` won't find a value type when you searched for a pointer. Pick pointer, stay pointer.

## `panic` and `recover` - the rare escape hatch

Everything so far has been *expected* failures - a missing file, garbage input, an absent user. Go has a *second*, separate mechanism for a different category of problem: `panic`.

📝 **`panic`** - stops normal execution immediately, runs deferred functions as it unwinds the stack, and crashes the program with a stack trace. **`recover`** - callable only inside a deferred function, catches a panic mid-unwind and lets the program carry on instead of dying.

The reason Go has these but says to almost never use them: errors are for failures you *expected could happen*; panic is for situations that *should be impossible*. A missing file is a Tuesday - return an error. An index out of range on a slice you just built, a `nil` pointer you swore was set, a `switch` hitting a `default` your own invariants say can't occur - those are *bugs*, states the author thought unreachable. There's no sensible "handle it and continue" for a violated assumption, so panic crashes loudly with a stack trace to fix it.

⚠️ **Gotcha - don't use panic for normal control flow.** Coming from exception-based languages it's tempting to `panic` on bad input and `recover` up top instead of threading `error` returns through. Resist it: panic skips the explicit, checkable error path Go is built around, unwinds invisibly across function boundaries, and - the real teeth - a panic escaping a goroutine crashes the *entire process*, since `recover` only works in the same goroutine that's unwinding. Errors-as-values is the road; panic is the emergency exit.

So when is `recover` legitimate? At a **boundary** where one unit of work must not take down the whole program - a server handling many requests. If one request panics from a bug deep in a handler, you'd rather fail *that one request* than crash the process for everyone else.

```go
package main

import "fmt"

// handle runs one request and converts any panic into a returned error,
// so a bug in handling one request can't crash the whole server.
func handle(req string) (err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("recovered from panic handling %q: %v", req, r)
		}
	}()

	if req == "bad" {
		panic("unexpected nil in handler") // simulate a bug deep in the call stack
	}
	fmt.Printf("handled %q OK\n", req)
	return nil
}

func main() {
	for _, req := range []string{"good", "bad", "good"} {
		if err := handle(req); err != nil {
			fmt.Println("ERROR:", err)
		}
	}
	fmt.Println("server still running")
}
```
```console
$ go run main.go
handled "good" OK
ERROR: recovered from panic handling "bad": unexpected nil in handler
handled "good" OK
server still running
```
*What just happened:* The `"bad"` request panicked - normally a program-ending event. But `handle`'s deferred `recover()` caught the panic value mid-unwind and turned it into an ordinary `error` assigned to the named return `err`. The panic was *contained* at the request boundary: that request failed, the loop continued, the next `"good"` request ran normally - the program survived. This recover-at-the-boundary pattern is the main legitimate use of `recover`: a safety net for bugs, not a handler for expected failures.

💡 **The rule, in one line.** Return an `error` for anything that could reasonably happen; `panic` only for "this should never happen." A sentence describing when the failure occurs normally means error; "a bug in my code" means panic.

## Recap

1. **Wrap to add context.** `fmt.Errorf("doing X: %w", err)` encloses an error in a new layer while keeping a link to the original, building an **error chain** that reads outside-in. `%w` preserves the chain; `%v` only formats text.
2. **Inspect with `errors.Is`.** Walks the whole chain to match a known **sentinel** value. ⚠️ Never use `==` on a wrapped error - it sees only the outermost layer.
3. **Extract with `errors.As`.** Walks the chain to find a specific error *type* and fills your variable so you can read its fields - the tool for **custom error types**.
4. **Sentinels vs. custom types.** Use a sentinel (`var ErrX = errors.New(...)`) for a plain known *condition*; use a custom struct error when callers need *structured data*. Sentinels are an API promise; types carry detail.
5. **`panic` is not error handling.** Return errors for expected failures; reserve `panic` for impossible states (bugs). ⚠️ A panic that escapes a goroutine crashes the whole process.
6. **`recover` at a boundary.** A deferred `recover()` can contain a panic at a request/job boundary so one bad unit of work doesn't kill the program - a safety net, not a control-flow tool.

You now make errors carry their own story and read it back in code. Next: the **runtime** - how the scheduler juggles goroutines onto OS threads, and how Go's memory and garbage collector keep it all fast.

## Quick check

Test yourself on the two ideas that do the heavy lifting here - wrapping and the panic/error divide:

```quiz
[
  {
    "q": "You have `err := fmt.Errorf(\"loading config: %w\", ErrNotFound)`. Which check correctly detects that `ErrNotFound` is in the chain?",
    "choices": [
      "errors.Is(err, ErrNotFound)",
      "err == ErrNotFound",
      "errors.As(err, ErrNotFound)",
      "err.Error() == ErrNotFound.Error()"
    ],
    "answer": 0,
    "explain": "errors.Is walks the entire chain and matches the sentinel underneath the wrapper. `==` is false because `err` is the outer wrapper, not the original; errors.As is for matching a type (and needs a pointer to a target); comparing message strings is fragile and not how identity works."
  },
  {
    "q": "When should you reach for a custom error type (a struct implementing `error`) instead of a sentinel error?",
    "choices": [
      "When callers need structured data about the failure (a field name, a code) that they extract with errors.As",
      "Whenever an error might be wrapped, since sentinels can't be wrapped",
      "Only inside deferred functions that call recover",
      "When you want the error to be faster to compare than a sentinel"
    ],
    "answer": 0,
    "explain": "A sentinel is one shared, immutable value - great for a yes/no condition, but it can't carry per-occurrence detail. A custom error type holds fields callers pull out with errors.As. (Both sentinels and custom types wrap fine, so that's not the deciding factor.)"
  },
  {
    "q": "Which situation is the appropriate use of `panic` rather than returning an error?",
    "choices": [
      "An invariant your own code guarantees is violated - a 'this should never happen' bug",
      "A user submitted a malformed form field",
      "A network request timed out",
      "A configuration file the program expects might be missing"
    ],
    "answer": 0,
    "explain": "panic is for impossible states - bugs where an assumption the author believed unreachable was violated. Malformed input, timeouts, and missing files are all expected, normal failures; those are 'Tuesdays' and should be returned as errors and checked with `if err != nil`."
  }
]
```


---

# The Runtime: Scheduler, Memory & GC - What Go Does for You

Back in the concurrency phases you spun up goroutines with a single keyword and never thought about the cost. You typed `go doWork()` a thousand times and your program didn't fall over. That's not luck - it's the Go **runtime**, machinery baked into every binary that schedules your goroutines, decides where values live, and quietly cleans up memory you stop using.

You can write Go for years without opening this hood. But "why are goroutines so cheap?", "why did memory usage climb and never come back down?", and "why is the compiler putting *this* on the heap?" are all runtime questions. The big idea: **Go trades a little magic you don't control for a lot of work you don't have to do** - and it pays off as long as you know the few places it can still bite.

## Why goroutines are cheap

The first thing to unlearn: a goroutine is **not** an operating-system thread. "Lightweight thread" is close enough to be dangerous - the same heavy object with a smaller label, which is wrong.

📝 **Goroutine** - a function managed by the Go runtime, not the OS. It starts with a tiny (~2 KB) stack that *grows on demand*, and the runtime - not the kernel - decides when it runs. **OS thread** - a unit the kernel schedules, with a large fixed stack (often ~1–8 MB) and an expensive kernel-space context switch.

Run the numbers: a thousand OS threads at 1 MB of stack each is a gigabyte reserved before a line of your code runs. A thousand goroutines at 2 KB each is about 2 MB, growing only if a goroutine needs a deeper stack. That's why "spawn a goroutine per request" is normal Go, and "spawn a thread per request" brings a server to its knees.

```go
package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	for i := 0; i < 100_000; i++ { // a hundred thousand goroutines - no problem
		wg.Add(1)
		go func(n int) {
			defer wg.Done()
			_ = n * n
		}(i)
	}
	wg.Wait()
	fmt.Println("all 100,000 goroutines finished")
}
```
```console
$ go run main.go
all 100,000 goroutines finished
```
*What just happened:* We launched a hundred thousand goroutines and the program shrugged - a hundred thousand OS threads would exhaust memory before they even started. Each goroutine began life with a stack measured in kilobytes; the kernel never saw 100,000 schedulable things, just a handful of threads - the trick we're about to unpack.

💡 **The insight.** Cheap goroutines aren't free - the runtime keeps the *expensive* resources (OS threads) small in number and reuses them, while the *cheap* things (goroutines) multiply freely. The scheduler makes that reuse possible.

## The GMP scheduler

With 100,000 goroutines and maybe 8 CPU cores, something has to decide which goroutine runs on which core, and when. That's the **GMP scheduler**, and once you know its three letters the whole design clicks.

📝 **G (goroutine)** - its stack, instruction pointer, and state; there can be hundreds of thousands. **M (machine)** - an OS thread, what the kernel actually schedules onto a CPU; there are few. **P (processor)** - a *logical* processor: a scheduling context holding a queue of runnable Gs and the resources an M needs to run them, set by `GOMAXPROCS` (default: CPU core count).

The shape to hold in your head: an **M must hold a P to run a G**. Ps are the permission slips - exactly `GOMAXPROCS` of them, so at most that many goroutines run *truly in parallel*, but the runtime cycles thousands of Gs through those few Ps fast enough that everything makes progress.

```mermaid
flowchart LR
  G1[G goroutines] --> P1[P run queue]
  G2[G goroutines] --> P1
  G3[G goroutines] --> P2[P run queue]
  P1 --> M1[M OS thread]
  P2 --> M2[M OS thread]
  M1 --> C[CPU cores]
  M2 --> C
```

Two mechanisms keep the cores busy:

- **Blocking syscalls don't block the core.** When a goroutine makes a blocking syscall (reading a file, waiting on the network), its M gets stuck in the kernel. Rather than let a precious P sit idle, the runtime **hands that P to another M**, which immediately runs other goroutines - your CPU never twiddles its thumbs because one goroutine is waiting on disk.
- **Work-stealing balances the load.** When a P empties its local run queue, instead of going idle it **steals** half the runnable goroutines from another P's queue - spreading work across cores without a single global lock to fight over.

```go
package main

import (
	"fmt"
	"runtime"
)

func main() {
	fmt.Println("GOMAXPROCS (Ps):", runtime.GOMAXPROCS(0)) // 0 = query, don't change
	fmt.Println("CPU cores:      ", runtime.NumCPU())
	go func() {}()
	fmt.Println("goroutines now: ", runtime.NumGoroutine())
}
```
```console
$ go run main.go
GOMAXPROCS (Ps): 8
CPU cores:       8
goroutines now:  2
```
*What just happened:* `GOMAXPROCS(0)` reports how many Ps exist - 8 here, matching the cores, so up to 8 goroutines run in parallel. `NumGoroutine()` shows 2: `main`'s goroutine plus the one launched. The takeaway: a small, fixed number of Ps governs parallelism, while the goroutine count balloons independently.

⚠️ **Gotcha - `GOMAXPROCS` is parallelism, not concurrency.** Setting `GOMAXPROCS=1` does *not* break a concurrent program; goroutines still interleave on that single P, it just limits how many run at the literal same instant. Concurrency (structure) and parallelism (simultaneous execution) are different knobs - see [Phase 12: Concurrency Patterns](12-concurrency-patterns.md) for the structure side.

## Stack vs heap - where your values live

The scheduler decides *when* code runs. The other half decides *where data lives* - the stack or the heap.

📝 **Stack** - a per-goroutine region that grows and shrinks with function calls. When a function returns, its slice of the stack vanishes instantly and for free - allocation is a pointer bump, deallocation automatic. **Heap** - a shared pool for values that must outlive their creating function; it's *not* auto-freed on return, reclaiming it is the garbage collector's job.

In many languages you choose: `int x` on the stack, `new Thing()` on the heap. In Go, **you don't choose - the compiler does.** Write `x := Thing{}` and Go figures out whether `x` can live and die on the stack (cheap) or must be promoted to the heap (more expensive, since the GC must track it). The compiler's rule: *if a value can outlive its function, it must go on the heap* - and the analysis that applies it has a name.

## Escape analysis

📝 **Escape analysis** - the compile-time pass deciding, for each value, whether it stays on the stack or must "escape" to the heap. A value escapes when the compiler can't prove it's done being used by the time its function returns - most commonly because a pointer to it leaks out.

The classic escape: returning a pointer to a local variable. It would normally die when the function returns, but handing its address to the caller means it *can't* - it lives on the heap instead.

```go
package main

type Point struct{ X, Y int }

// stays: the Point is copied out by value, the local can die on return
func makeValue() Point {
	p := Point{1, 2}
	return p
}

// escapes: we return a pointer, so p must outlive makeValue → heap
func makePointer() *Point {
	p := Point{3, 4}
	return &p
}

func main() {
	_ = makeValue()
	_ = makePointer()
}
```

You don't have to guess what the compiler decided - ask it. The `-gcflags=-m` flag prints escape-analysis decisions:

```console
$ go build -gcflags=-m main.go
./main.go:13:2: moved to heap: p
./main.go:14:9: &p escapes to heap
```
*What just happened:* The compiler reported `p` inside `makePointer` **moved to heap** because its address escapes via `return &p`. It said *nothing* about `makeValue`'s `p`, which stayed on the stack and vanished for free. Same-looking code, two fates, decided entirely by whether a pointer leaked. (Returning a *pointer* can be more expensive than a *value* here, precisely because it forces a heap allocation.)

💡 **The insight.** Fewer escapes means fewer heap allocations means less GC work - *the* lever behind most Go performance tuning. When a hot path is slow, run `-gcflags=-m`, find values escaping inside your tight loop, and restructure to keep them on the stack. You rarely fight the GC directly - you reduce the garbage it has to collect. (You'll measure exactly this with the profiler in [Phase 15](15-testing-benchmarks-profiling.md).)

## The garbage collector

Everything that escapes to the heap eventually stops being used. Something has to reclaim it, and in Go that's automatic.

📝 **Garbage collector (GC)** - the runtime component that finds heap memory your program can no longer reach and frees it, so you never call `free()` yourself. Go's GC is a **concurrent, tri-color mark-and-sweep tracing** collector tuned for *low pause times*.

The model: the GC works by **reachability**. Starting from the roots (global variables, everything on every goroutine's stack), it traces every pointer it can follow. Anything reached is *live* and kept; anything unreachable is garbage, swept back into the pool. You don't track lifetimes; reachability *is* the lifetime.

The "concurrent, low-pause" part makes it pleasant. Older GCs would freeze the entire program ("stop the world") while marking, causing visible hiccups. Go's GC marks **concurrently, while your program keeps running**, reserving stop-the-world for two brief phases at the start and end - typically sub-millisecond even on large heaps, optimizing for "no big stalls" over "absolute minimum CPU."

One main dial: the `GOGC` environment variable (default `100`), controlling the memory-vs-CPU trade-off. `GOGC=100` lets the heap grow to roughly double the live set before the next collection. Raise it (`GOGC=200`) and the GC runs less often, using more memory but less CPU; lower it (`GOGC=50`) for the opposite. Most programs never touch it.

Watch the heap grow and the GC reclaim it:

```go
package main

import (
	"fmt"
	"runtime"
)

func heapBytes() uint64 {
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	return m.HeapAlloc
}

func main() {
	fmt.Printf("before: %d KB\n", heapBytes()/1024)

	junk := make([][]byte, 0, 1000)
	for i := 0; i < 1000; i++ {
		junk = append(junk, make([]byte, 10_000)) // ~10 MB of reachable garbage-to-be
	}
	fmt.Printf("after allocating: %d KB\n", heapBytes()/1024)

	junk = nil      // drop the only reference → all of it is now unreachable
	runtime.GC()    // force a collection (normally you'd never call this)
	fmt.Printf("after GC: %d KB\n", heapBytes()/1024)
}
```
```console
$ go run main.go
before: 96 KB
after allocating: 9863 KB
after GC: 102 KB
```
*What just happened:* Allocating a thousand 10 KB slices pushed the live heap to ~9.8 MB. The instant we set `junk = nil`, nothing could reach those slices anymore. The next collection traced from the roots, found none of them, and swept the memory back near its starting size - we never freed anything by hand. (`runtime.GC()` here just makes the timing visible; real code lets the GC decide when to run, driven by `GOGC`.)

Step through the mark-and-sweep cycle - roots, reachable, and swept - at your own pace:

```playground-gc
```

⚠️ **Gotcha - you can still "leak."** Automatic GC doesn't mean leak-proof - it only frees what's *unreachable*, so accidentally keeping a reference alive keeps the memory forever, looking exactly like a leak. Two classic culprits:

```go
// 1. A goroutine that never exits, holding memory the whole time.
func leakyGoroutine(data []byte) {
	go func() {
		<-make(chan struct{}) // blocks forever; nothing ever sends
		_ = data              // `data` is reachable as long as this goroutine lives
	}()
}

// 2. A global map that only ever grows.
var cache = map[string][]byte{}

func remember(key string, val []byte) {
	cache[key] = val // never deleted → cache (and its memory) grows without bound
}
```
*What just happened:* The goroutine blocks on a channel that never receives, so it never returns - and because it closes over `data`, that slice stays reachable forever. A pile of such stuck goroutines is the most common real Go memory leak. The global `cache` similarly keeps every value reachable for the program's life; without an eviction policy it grows until memory runs out. The GC does its job perfectly in both cases - the memory genuinely *is* still reachable. The fix isn't a GC setting; it's making sure goroutines can exit (a `context` or done channel, from [Phase 12](12-concurrency-patterns.md)) and long-lived maps have a bound.

## Recap

1. A **goroutine is not an OS thread** - it starts at ~2 KB with a growable stack, scheduled by the runtime, which is why a program can run hundreds of thousands where it could afford only a handful of threads.
2. The **GMP scheduler** multiplexes many **G**s onto few **M**s (OS threads) via **P**s (logical processors, capped by `GOMAXPROCS`). Handing off Ps on blocking syscalls and work-stealing keep every core busy.
3. Every value lives on the **stack** (cheap, auto-freed on return) or the **heap** (needs the GC). **Escape analysis** - visible via `go build -gcflags=-m` - decides which; a value escapes when it can outlive its function (e.g. returning a pointer to a local).
4. Go's **garbage collector** is concurrent, low-pause, mark-and-sweep, and reachability-based: it frees what your program can no longer reach, so you never call `free()`. `GOGC` tunes the memory-vs-CPU trade-off.
5. ⚠️ Automatic GC is **not leak-proof** - anything still *reachable* is kept, so stuck goroutines and ever-growing global maps cause real memory growth. Fewer heap escapes and bounded lifetimes, not GC settings, are your main levers.

You now know what happens beneath `go`, `make`, and `&`. Next: making it measurable - testing, benchmarking, and profiling, where you'll watch allocations and CPU time with real tools instead of reasoning in the abstract.

## Quick check

Test yourself on the three ideas that matter most - cheap goroutines, the GMP roles, and what "escape" really means:

```quiz
[
  {
    "q": "Why can a Go program run hundreds of thousands of goroutines but not hundreds of thousands of OS threads?",
    "choices": [
      "A goroutine starts with a tiny (~2 KB) growable stack and is scheduled by the runtime, while each OS thread reserves a large fixed stack and is scheduled by the kernel",
      "Goroutines run on the GPU instead of the CPU, which has far more cores",
      "The Go compiler converts goroutines into a single thread, so there's really only ever one",
      "Goroutines don't use memory at all until they finish running"
    ],
    "answer": 0,
    "explain": "A goroutine begins life at roughly 2 KB with a stack that grows on demand and is multiplexed onto a few OS threads by the runtime. OS threads each reserve a large fixed stack (often 1–8 MB) and carry kernel-scheduling overhead, so thousands of them exhaust memory fast."
  },
  {
    "q": "In the GMP scheduler, what is a P?",
    "choices": [
      "A logical processor: a scheduling context with a run queue of goroutines, capped by GOMAXPROCS, that an M must hold to run a G",
      "The pointer to a goroutine's stack",
      "A physical CPU core, exactly one per chip",
      "A 'pending' goroutine that is blocked on a channel"
    ],
    "answer": 0,
    "explain": "G is a goroutine, M is an OS thread, and P is a logical processor - a scheduling context holding a run queue. An M must hold a P to run a G, and the number of Ps (GOMAXPROCS) sets how many goroutines run truly in parallel."
  },
  {
    "q": "According to escape analysis, why does returning `&p` (a pointer to a local) force `p` onto the heap?",
    "choices": [
      "Because p must outlive the function that created it, so it can't die with the stack frame on return",
      "Because pointers are always stored on the heap in every language",
      "Because the garbage collector refuses to track stack memory",
      "Because returning a pointer is a compile error unless the value is heap-allocated"
    ],
    "answer": 0,
    "explain": "A stack-allocated local would vanish when its function returns. Returning its address means the caller can still use it afterward, so the value must outlive the frame - the compiler 'moves it to heap.' Returning the value by copy instead lets it stay on the stack."
  }
]
```


---

# Testing, Benchmarks & Profiling - Proving It Works and Finding the Slow Part

Back in [Phase 8](08-ecosystem-and-tooling.md) you saw the *shape* of a Go test: a function named `TestXxx(t *testing.T)`, a plain `if`, a `t.Errorf` when reality disagreed. Enough to write your first test - not enough to test a real codebase without drowning.

This phase is the rest of the iceberg. The same `testing` package - no plugins, no DSL - also gives a clean way to run dozens of cases through one function, a way to *measure* how fast and memory-hungry your code is, and a way to *find* the slow part instead of guessing. The mental model: **the Go toolchain refuses to let you operate on vibes.** It pushes you to write the cases down, measure the numbers, look at where the time went. Once internalized, "is it correct?" and "is it fast?" stop being arguments and become commands you run.

## Table-driven tests - the idiomatic Go pattern

**What it actually is.** A table-driven test holds a *slice of test cases* and loops over them, running the same assertion logic against each. Each case is a little struct: inputs, expected output, a name. Instead of copy-pasting the same three lines per scenario, you add a row.

📝 **Table-driven test** - a test where the cases live in data (a slice of structs), and one loop runs every case through the same check. Adding a scenario means a new row, not a new function.

**Why Go prefers this over assertion libraries.** Coming from other languages, you might expect `assertThat(x).isEqualTo(6)`. Go deliberately doesn't ship that, and the community mostly doesn't want it: an assertion library is a second little language to learn, with failure messages written by someone else. A table-driven test is *just Go* - a slice, a `for` loop, an `if`. Data and logic stay separate, so the table reads like a spec you could hand to a stranger, and you control exactly what a failure says.

**A real example.** Testing a `Clamp` function that pins a number into a `[min, max]` range:

```go
// clamp.go
package mathx

func Clamp(n, min, max int) int {
	if n < min {
		return min
	}
	if n > max {
		return max
	}
	return n
}
```

```go
// clamp_test.go
package mathx

import "testing"

func TestClamp(t *testing.T) {
	cases := []struct {
		name           string
		n, min, max    int
		want           int
	}{
		{"inside range", 5, 0, 10, 5},
		{"below min", -3, 0, 10, 0},
		{"above max", 99, 0, 10, 10},
		{"equal to min", 0, 0, 10, 0},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got := Clamp(tc.n, tc.min, tc.max)
			if got != tc.want {
				t.Errorf("Clamp(%d, %d, %d) = %d; want %d",
					tc.n, tc.min, tc.max, got, tc.want)
			}
		})
	}
}
```

```console
$ go test -v ./...
=== RUN   TestClamp
=== RUN   TestClamp/inside_range
=== RUN   TestClamp/below_min
=== RUN   TestClamp/above_max
=== RUN   TestClamp/equal_to_min
--- PASS: TestClamp (0.00s)
    --- PASS: TestClamp/inside_range (0.00s)
    --- PASS: TestClamp/below_min (0.00s)
    --- PASS: TestClamp/above_max (0.00s)
    --- PASS: TestClamp/equal_to_min (0.00s)
PASS
ok  	example/mathx	0.004s
```

*What just happened:* We declared an anonymous struct slice - the table - with one row per scenario. The `for` loop walked every row and ran the *same* check. Adding a fifth case is one new line, not a whole new function. `-v` printed each case as it ran, and because each row got its own `t.Run`, the output is a neat tree, not a wall of undifferentiated PASSes.

💡 **Key point.** The win isn't just less typing - the *behavior under test is now a visible list*. A reviewer can scan the table and ask "where's the case for `min > max`?" - much harder when each scenario is buried in its own function.

## Subtests, helpers, and parallelism

The `t.Run(name, func)` you just used unlocks three more things.

**Subtests (`t.Run`).** Each `t.Run` is an independent sub-test with its own name and pass/fail. Crucially, you can run *just one* without running the rest - names are addressable:

```console
$ go test -run TestClamp/below_min -v ./...
=== RUN   TestClamp
=== RUN   TestClamp/below_min
--- PASS: TestClamp (0.00s)
    --- PASS: TestClamp/below_min (0.00s)
PASS
```

*What just happened:* `-run` takes a regex matched against test names, and `/` reaches inside a parent into one subtest - how you re-run *only* a failing row in a tight loop instead of the whole suite.

**Helpers (`t.Helper`).** When you extract repeated assertion logic into a helper function, call `t.Helper()` at the top - it tells the testing package "blame the *caller's* line when I fail, not mine."

```go
func assertEqual(t *testing.T, got, want int) {
	t.Helper() // failures report the caller's line number, not this one
	if got != want {
		t.Errorf("got %d; want %d", got, want)
	}
}
```

*What just happened:* Without `t.Helper()`, every failure points inside `assertEqual` - useless, since they all look identical. With it, the failure points at the line in your test that called the helper.

**Parallelism (`t.Parallel`).** Calling `t.Parallel()` inside a subtest signals it's safe to run alongside other parallel tests, speeding up an I/O-heavy suite. ⚠️ But parallel tests share the process: two touching the same global or file creates a race. Reach for it only when each case is genuinely independent, and run `go test -race` to catch the cases where it isn't.

## Benchmarks - measuring speed and allocations

Correctness is one question. *Speed* is different, and Go answers it with the same `testing` package.

📝 **Benchmark** - a function named `BenchmarkXxx(b *testing.B)` that the toolchain runs many times to measure how long one operation takes. The key piece: `for i := 0; i < b.N; i++` - put the code-under-test inside it, and Go chooses `b.N`, dialing it up until timing is statistically stable.

**Why the `b.N` loop is shaped that way.** You can't time a single function call reliably - it's over in nanoseconds, swamped by clock noise. Go runs your operation `b.N` times (maybe millions), measures the total, and divides. You never set `b.N` yourself; the framework starts small, sees how long that took, and scales up until it trusts the average.

💡 **Newer idiom (Go 1.24+).** `for b.Loop() { ... }` does the same job and is now the recommended form: it manages the count for you, keeps any setup before the loop out of the timed region, and stops the compiler from optimizing the measured code away. `b.N` still works everywhere and fills most existing code, so it's worth reading both - the examples below use `b.N`.

**A real example.** Benchmarking two ways of building a string from a slice - naive `+=` concatenation versus `strings.Builder`:

```go
// build_test.go
package strbuild

import (
	"strings"
	"testing"
)

var parts = []string{"a", "b", "c", "d", "e", "f", "g", "h"}

func BenchmarkConcat(b *testing.B) {
	for i := 0; i < b.N; i++ {
		s := ""
		for _, p := range parts {
			s += p // re-allocates the whole string every time
		}
		_ = s
	}
}

func BenchmarkBuilder(b *testing.B) {
	for i := 0; i < b.N; i++ {
		var sb strings.Builder
		for _, p := range parts {
			sb.WriteString(p)
		}
		_ = sb.String()
	}
}
```

```console
$ go test -bench=. -benchmem ./...
goos: linux
goarch: amd64
pkg: example/strbuild
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkConcat-12     8123140    142.6 ns/op    104 B/op    7 allocs/op
BenchmarkBuilder-12   19543210     61.3 ns/op     56 B/op    3 allocs/op
PASS
ok  	example/strbuild	2.913s
```

*What just happened:* `go test -bench=.` ran every benchmark, `-benchmem` added the memory columns:

- **`ns/op`** - nanoseconds per operation. `Builder` is ~61 ns vs `Concat`'s ~143 ns: more than twice as fast.
- **`B/op`** - bytes allocated per operation. `Builder` allocates 56 bytes; `Concat` allocates 104.
- **`allocs/op`** - *number* of heap allocations per operation. The killer column: `Concat` does 7 (one per `+=`, since strings are immutable so each builds a brand-new string), `Builder` does 3.

The `-12` suffix is the `GOMAXPROCS` the benchmark ran with. The lesson: `+=` in a loop quietly re-allocates the whole string every iteration, and the benchmark makes that invisible cost visible.

💡 **Key point.** `allocs/op` is usually the number to watch first. Allocations create GC work ([Phase 14](14-runtime-scheduler-and-memory.md)), so cutting them often improves the whole program's tail latency. A change that halves `ns/op` but doubles `allocs/op` may be a bad trade under real load.

## Profiling with pprof - measure, don't guess

A benchmark tells you *that* a function is slow, not *where inside it* the time goes. For that, Go has **pprof**.

📝 **pprof** - a profiler (and its analysis tool, `go tool pprof`) that samples your running program to record where it spends CPU time or allocates memory, then lets you inspect the result as a ranked list, call graph, or annotated source. "Profile" is the recorded data; "pprof" is the tool that reads it.

**The mental model: stop guessing.** Every engineer has a confident hunch about the bottleneck, wrong often enough to be dangerous. You optimize the function you *suspected*, ship it, and the program is exactly as slow as before, because the real cost was somewhere you never looked. A profiler replaces the hunch with a measurement, reporting in ranked order where time and memory truly went. The discipline: **profile first, then optimize the thing the profile points at.**

**Capturing a profile from a benchmark.** `go test` can dump a CPU profile while it runs:

```console
$ go test -bench=BenchmarkConcat -cpuprofile=cpu.out ./...
...
$ go tool pprof cpu.out
File: strbuild.test
Type: cpu
Entering interactive mode (type "help" for commands)
(pprof) top5
Showing nodes accounting for 2.31s, 91.3% of 2.53s total
      flat  flat%   sum%        cum   cum%
     1.04s 41.1%  41.1%      1.04s 41.1%  runtime.concatstrings
     0.62s 24.5%  65.6%      0.71s 28.1%  runtime.mallocgc
     0.31s 12.3%  77.9%      0.31s 12.3%  runtime.memmove
     0.21s  8.3%  86.2%      0.21s  8.3%  runtime.nextFreeFast
     0.13s  5.1%  91.3%      2.18s 86.2%  strbuild.BenchmarkConcat
(pprof) 
```

*What just happened:* `-cpuprofile=cpu.out` wrote a profile during the benchmark; `go tool pprof cpu.out` opened it interactively. `top5` ranked functions by **`flat`** time - time spent *in that function itself*, not its callees: 41% into `runtime.concatstrings`, 25% into `runtime.mallocgc` (the allocator). The profile *confirms* the benchmark's hint: this code's cost is string concatenation and the allocations it triggers. `cum` (cumulative) counts a function plus everything it calls, why `BenchmarkConcat` shows a low flat but high cum.

**Profiling a live server.** For long-running services, import `net/http/pprof`, registering profiling endpoints on your HTTP server:

```go
import (
	"net/http"
	_ "net/http/pprof" // blank import: registers /debug/pprof/ handlers as a side effect
)

// ... with your server running, profiles are now served under /debug/pprof/
```

```console
$ go tool pprof http://localhost:8080/debug/pprof/profile?seconds=30
```

*What just happened:* The blank import (`_`) pulls in the package for its `init()` side effect, wiring up handlers under `/debug/pprof/`. `go tool pprof` then collected 30 seconds of live CPU samples and dropped you into the same interactive analysis. (Inside pprof, `web` renders a visual call graph and `list <func>` shows source annotated with per-line cost.)

⚠️ Don't expose `/debug/pprof/` on a public interface - it leaks internals and lets anyone trigger expensive profiles. Bind it to localhost, an admin port, or behind authentication.

This sets up the next phase: profiling is the *evidence-gathering* step. [Phase 17](17-performance-and-optimization.md) covers what to *do* once the profile tells you where the time is.

## Coverage and fuzzing - what tests miss, and finding inputs you didn't

**Coverage** measures how much of your code your tests actually exercise. The toolchain tracks which lines ran:

```console
$ go test -cover ./...
ok  	example/mathx	0.004s	coverage: 87.5% of statements

$ go test -coverprofile=cover.out ./...
$ go tool cover -html=cover.out   # opens a browser: green = covered, red = not
```

*What just happened:* `-cover` printed the share of statements that ran. `-coverprofile` wrote per-line detail to a file, and `go tool cover -html` turned it into a color-coded view: green lines exercised, red never ran. The red is the useful part - it shows the branches your tests forgot.

⚠️ **100% coverage is not "bug-free."** The trap everyone walks into. Coverage tells you a line *executed*, nothing about whether you *checked the result* or tried the inputs that break it. A test that calls `Clamp` once and asserts nothing lights up the whole function green. Treat coverage as a *map of the untested* - chase the red - not a score to max out. High coverage with weak assertions is worse than plain medium coverage, because it *feels* safe.

**Fuzzing** attacks the other blind spot: inputs you never thought to write down. A **fuzz test** generates *random, mutating* inputs, hunting for one that crashes your function or breaks an asserted property.

📝 **Fuzzing** - automated testing where the framework feeds your function a flood of generated inputs (evolved from "seed" examples) to discover ones that cause a panic or violate an invariant. In Go it's built in: a `FuzzXxx(f *testing.F)` function.

```go
// reverse_test.go
package strbuild

import (
	"testing"
	"unicode/utf8"
)

func Reverse(s string) string {
	r := []rune(s)
	for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
		r[i], r[j] = r[j], r[i]
	}
	return string(r)
}

func FuzzReverse(f *testing.F) {
	f.Add("hello")        // seed corpus: starting examples
	f.Add("Göteborg")     // includes multi-byte runes on purpose
	f.Fuzz(func(t *testing.T, s string) {
		if !utf8.ValidString(s) {
			return // []rune can't round-trip invalid UTF-8, so skip it
		}
		// property: reversing twice returns the original
		if Reverse(Reverse(s)) != s {
			t.Errorf("double reverse changed %q", s)
		}
	})
}
```

```console
$ go test -fuzz=FuzzReverse
fuzz: elapsed: 0s, gathering baseline coverage: 0/192 completed
fuzz: elapsed: 3s, execs: 412903 (137621/sec), new interesting: 18
...
fuzz: elapsed: 12s, execs: 1843201 (no new interesting), 0 failures
^C
```

*What just happened:* `f.Add` seeded a few starting strings, and `f.Fuzz` ran a *property check* - "reversing twice gets the original back" - against a torrent of generated inputs. We asserted a property rather than specific outputs, since we don't know in advance what Go will invent. We skip invalid UTF-8 because `[]rune` can't round-trip it - drop that one guard and the fuzzer finds a failing input within seconds, which is exactly the kind of edge case it exists to surface. If a generated string ever broke the property, Go would stop, *shrink* it to the smallest failing input, and save it as a permanent regression test. (A normal `go test` still runs the seed corpus as ordinary cases - fuzzing only does open-ended generation under `-fuzz`.)

💡 **Key point.** Coverage asks "what code did my chosen inputs reach?" Fuzzing asks "what inputs did I never think to choose?" Coverage finds untested *code*; fuzzing finds untested *cases* - the empty string, the lone multi-byte rune, the overflow. Complementary; neither alone makes your code safe.

## Recap

1. **Table-driven tests** put your cases in a slice of structs and loop one assertion over all of them - adding a scenario is a new row, not a new function. Go prefers this to assertion libraries because it's *just Go*: readable as a spec, with failure messages you control.
2. **`t.Run`** creates addressable subtests (re-run one with `-run TestX/case`), **`t.Helper()`** makes a helper's failures point at the caller, and **`t.Parallel()`** opts a test into concurrent execution (pair it with `-race`).
3. **Benchmarks** (`BenchmarkXxx(b *testing.B)`) run your code `b.N` times - a count Go chooses - and `go test -bench=. -benchmem` reports **ns/op**, **B/op**, and the all-important **allocs/op**.
4. **pprof** replaces guesswork with measurement: capture a profile (`-cpuprofile` from a benchmark, or `net/http/pprof` on a server), open it with `go tool pprof`, and let `top`/`list` point you at the real hot spot before you optimize anything.
5. **Coverage** (`go test -cover`) maps which lines ran - use it to find the *red*, never as a score, because ⚠️ 100% coverage with weak assertions proves nothing.
6. **Fuzzing** (`FuzzXxx(f *testing.F)`) generates inputs you'd never write by hand and checks a *property*; it finds the edge case, shrinks the failure, and saves it as a regression.

You can now prove your Go is correct, measure how fast it is, and find the slow part with evidence instead of instinct. Next: that lens turned on the standard library itself, a masterclass in Go's design philosophy.

## Quick check

Test yourself on the ideas that separate "I ran a test" from "I measured my program":

```quiz
[
  {
    "q": "In a benchmark, why do you wrap the code under test in `for i := 0; i < b.N; i++`?",
    "choices": [
      "Go runs the operation b.N times - a count it picks automatically - and divides, so a single fast call isn't swamped by clock noise",
      "b.N is a constant you must set to the number of CPU cores",
      "The loop makes the benchmark allocate less memory",
      "It's required syntax with no effect on the measurement"
    ],
    "answer": 0,
    "explain": "A single nanosecond-scale call can't be timed reliably. Go dials b.N up until the total run time is statistically stable, then reports the per-operation average. You never set b.N yourself - you just make the loop body do exactly the work you want measured."
  },
  {
    "q": "Your test suite reports 100% coverage. What does that actually guarantee?",
    "choices": [
      "Every line of code executed at least once during the tests - nothing about whether results were checked",
      "The code has no bugs",
      "Every possible input was tested",
      "All assertions in the tests passed correctly"
    ],
    "answer": 0,
    "explain": "Coverage only measures which lines ran. A test that calls a function and asserts nothing still marks those lines green. Treat coverage as a map of the untested (chase the red); high coverage with weak assertions feels safe but proves almost nothing."
  },
  {
    "q": "You suspect a function is your bottleneck. What does pprof give you that a benchmark doesn't?",
    "choices": [
      "It samples the real run and ranks where time and memory actually went, so you optimize the proven hot spot instead of your hunch",
      "It automatically rewrites the slow function to be faster",
      "It guarantees the function has no allocations",
      "It only works on functions named BenchmarkXxx"
    ],
    "answer": 0,
    "explain": "A benchmark tells you that something is slow; pprof tells you where inside it the time goes. The discipline is to profile first and then optimize the thing the profile points at - because the function you suspected is wrong often enough to waste real effort."
  }
]
```


---

# The Standard Library as Design - Small Interfaces, Big Reach

Most languages ship a standard library that feels like a junk drawer - modules accreted over decades, each with its own opinions and quirks. Go's feels *designed*. Pick up almost any package and you find the same handful of small ideas reused everywhere, so learning one corner teaches you the next.

The mental model: **Go's standard library is built on a few tiny interfaces that compose into enormous reach.** Phases [9](09-idioms-and-gotchas.md) and [10](10-interfaces-in-depth.md) showed Go interfaces are small and satisfied implicitly. This phase shows what that buys a *whole ecosystem*: a file, a network connection, an in-memory buffer, and a gzip compressor can all plug into each other, because every one speaks the same one-method language. Once you see the pattern, you'll stop reaching for third-party packages reflexively - the stdlib has probably already solved it, coherently.

## `io.Reader` and `io.Writer` - two methods that run the I/O world

**What it actually is.** Nearly all Go input and output flows through two interfaces, each with exactly *one* method:

```go
type Reader interface {
	Read(p []byte) (n int, err error)
}

type Writer interface {
	Write(p []byte) (n int, err error)
}
```

A `Reader` is "anything you can pull bytes *from*." A `Writer` is "anything you can push bytes *to*." That's the entire contract. Because satisfaction is implicit (Phase 9), a staggering number of unrelated types end up being readers and writers without saying so: an open file, a TCP connection, a `bytes.Buffer`, an HTTP request body, a gzip stream, even `os.Stdout`.

📝 **Terminology.** `io.Reader` / `io.Writer` are the two foundational interfaces in `io`. A type "is a reader" by having a `Read([]byte) (int, error)` method - no inheritance, no registration. Same for writers.

**Why this is the whole game.** Because every source is a `Reader` and every destination a `Writer`, *any* source can connect to *any* destination. No `copyFileToSocket` and separate `copyBufferToFile` - one `io.Copy(dst Writer, src Reader)` works for every combination, including ones nobody anticipated. The pieces compose like Lego: same studs.

```mermaid
flowchart LR
  F[os.File] --> R{io.Reader}
  N[net.Conn] --> R
  B[bytes.Buffer] --> R
  R --> CP[io.Copy]
  CP --> W{io.Writer}
  W --> GZ[gzip.Writer]
  W --> F2[os.File]
  W --> O[os.Stdout]
```

*One idea:* anything on the left flows into anything on the right, since the middle only ever talks about `Reader` and `Writer`. A gzip writer is *itself* a writer wrapping another writer, so you can stack them.

**A real example.** One piece of plumbing, two very different destinations:

```go
package main

import (
	"bytes"
	"io"
	"os"
	"strings"
)

func main() {
	src := strings.NewReader("hello, readers and writers\n") // a Reader over a string

	var buf bytes.Buffer        // an in-memory Writer
	io.Copy(&buf, src)          // pump everything from src into buf

	// Reset and copy the same kind of source straight to the terminal.
	src2 := strings.NewReader("...and now to stdout\n")
	io.Copy(os.Stdout, src2)    // os.Stdout is a Writer too

	os.Stdout.WriteString(buf.String())
}
```
```console
$ go run main.go
...and now to stdout
hello, readers and writers
```
*What just happened:* `strings.NewReader` gave a `Reader` over a plain string. `io.Copy` doesn't know or care what's on either end - it pulled bytes from the reader and pushed them to the writer until EOF, once into a `bytes.Buffer`, once into `os.Stdout`, changing *nothing* about the copy logic. The buffer and the terminal share nothing except satisfying `io.Writer` - all `io.Copy` ever asked for.

💡 **Key point.** This is interface-driven design from [Phase 10](10-interfaces-in-depth.md), at the scale of an entire standard library. Functions are written against the *smallest* interface they need (`io.Copy` needs only `Read` and `Write`), accepting the widest set of types - including types written years later. Wrapping is the superpower: a `gzip.Writer` compresses on its way to *another* writer, so `gzip.NewWriter(file)` gives "compress, then write to disk" by stacking two writers.

## `context` - carrying cancellation across boundaries

You met `context` in [Phase 12](12-concurrency-patterns.md) for stopping work. It deserves a spot here because `context.Context` is itself a tiny interface the rest of the library threads through everything.

**What it actually is.** A `Context` carries three things across API boundaries: a **cancellation signal** (a `Done()` channel that closes when work should stop), an optional **deadline**, and **request-scoped values**. By convention it's the *first* parameter of any cancellable function: `func Fetch(ctx context.Context, url string) (...)`.

**Why this is a design lesson.** Because `net/http`, database drivers, and most well-behaved libraries all accept a `context.Context`, cancellation composes the same way `io` does. A single timeout set at the top flows down through the HTTP handler, into the database query, into the outbound API call - every layer watches the same `Done()` channel, giving cancellation across the entire call tree for free.

```go
package main

import (
	"context"
	"fmt"
	"time"
)

func slowWork(ctx context.Context) error {
	select {
	case <-time.After(2 * time.Second): // pretend this takes 2s
		return nil
	case <-ctx.Done(): // ...but the context gave up first
		return ctx.Err()
	}
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
	defer cancel() // always release the context's resources

	if err := slowWork(ctx); err != nil {
		fmt.Println("stopped:", err)
	}
}
```
```console
$ go run main.go
stopped: context deadline exceeded
```
*What just happened:* `context.WithTimeout` made a context that cancels itself after 100ms. `slowWork` raced two channels in a `select`: real work (2 seconds) versus `Done()`. The deadline fired first, so `slowWork` returned `ctx.Err()` - `context deadline exceeded` - instead of blocking two seconds. Pass that same `ctx` to an `http.Request` or SQL query and they'd abandon work at the same mark. ⚠️ Always `defer cancel()`; skipping it leaks the timer.

## `encoding/json` - structs in, JSON out

JSON is how most services talk, and `encoding/json` is how Go speaks it - mapping structs to JSON using *struct tags* and the same exported/unexported visibility rule from [Phase 9](09-idioms-and-gotchas.md).

**What it actually is.** `json.Marshal` turns a Go value into JSON bytes; `json.Unmarshal` parses it back. Control how a field appears with a **struct tag**: `json:"name"`. No schema file, no codegen - tags live right on the struct.

**A real example.**

```go
package main

import (
	"encoding/json"
	"fmt"
)

type User struct {
	Name  string `json:"name"`
	Email string `json:"email,omitempty"` // omit if empty
	Age   int    `json:"age"`
	token string // unexported - invisible to JSON
}

func main() {
	u := User{Name: "Ada", Age: 36, token: "secret"}

	data, _ := json.Marshal(u)
	fmt.Println(string(data))

	var back User
	json.Unmarshal([]byte(`{"name":"Grace","age":85}`), &back)
	fmt.Printf("%+v\n", back)
}
```
```console
$ go run main.go
{"name":"Ada","age":36}
{Name:Grace Email: Age:85 token:}
```
*What just happened:* `Marshal` walked the struct's *exported* fields using each `json:` tag for the key - `Name` became `"name"`. `Email` had `omitempty` and was empty, so it vanished entirely. The lowercase `token` field never appeared. `Unmarshal` parsed a JSON object into a fresh `User` (note `&back` needs a pointer to write into), filling `Name` and `Age`, leaving the rest at zero values.

⚠️ **Gotcha - unexported fields are invisible to JSON.** The same capitalization rule governing package visibility bites people wondering why their `password` field "disappeared." `encoding/json` lives in a different package and can only see *exported* (capitalized) fields. Watch `omitempty` too: it drops the field at the type's *zero value*, so a real, intentional `0`, `false`, or `""` also disappears. Reach for a pointer (`*int`) to distinguish "absent" from "zero."

## `net/http` - a real web server in a few lines

The headline proof Go's standard library is production-grade: a genuine, deployable HTTP server with no framework at all - built on the interfaces you've already met. `http.ResponseWriter` *is* an `io.Writer`; a request body *is* an `io.Reader`; every handler takes a `context.Context` via the request.

**What it actually is.** Register handlers on an `http.ServeMux` (a router mapping URL paths to functions), then hand the mux to `http.ListenAndServe`. A handler receives an `http.ResponseWriter` and an `*http.Request`.

**A real example - server and client in one file.**

```go
package main

import (
	"fmt"
	"io"
	"net/http"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "hello from the stdlib") // w is an io.Writer!
	})

	go http.ListenAndServe(":8080", mux) // run the server in the background

	// Now act as a client against our own server.
	resp, err := http.Get("http://localhost:8080/hello")
	if err != nil {
		fmt.Println("request failed:", err)
		return
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body) // resp.Body is an io.Reader!
	fmt.Print(string(body))
}
```
```console
$ go run main.go
hello from the stdlib
```
*What just happened:* We built a router with `http.NewServeMux`, registered a handler on `/hello`, started the server with `http.ListenAndServe`. Inside, `w` is an `http.ResponseWriter` satisfying `io.Writer`, so `fmt.Fprintln(w, ...)` writes the response exactly like writing to a file. `http.Get` made a client request, and `resp.Body` came back as an `io.Reader`, drained by `io.ReadAll`. The same two interfaces from the start of this phase carry the entire request and response. (Always `defer resp.Body.Close()` to free the connection.)

💡 **Key point.** This server is not a toy. `net/http` handles connection management, HTTP/1.1 and HTTP/2, TLS, timeouts, and concurrency (each request in its own goroutine) - production services with no framework underneath. For a focused JSON-over-HTTP walkthrough see [/guides/http-and-json-api-basics](/guides/http-and-json-api-basics).

## The lesson: reach for the standard library first

`io`, `context`, `encoding/json`, and `net/http` are the headliners, but the same coherence runs through the daily-driver packages you'll lean on constantly:

- **`time`** - durations, deadlines, timers, formatting (the source of `context`'s deadlines).
- **`strings`** and **`bytes`** - mirror-image toolkits for text and raw bytes; `strings.Builder` and `bytes.Buffer` are both writers.
- **`bufio`** - buffered wrappers around any reader/writer (you met `bufio.Scanner` in [Phase 7](07-errors-and-io.md)); it *wraps* an `io.Reader`, naturally.
- **`sort`** and the newer **`slices`** / **`maps`** generic helpers - sorting, searching, and transforming collections.
- **`errors`** - `errors.Is` / `errors.As` / `%w` wrapping from [Phase 7](07-errors-and-io.md), the error half of the same design philosophy.

The meta-point: **the standard library is coherent because it's built on a few small, composable interfaces, and that coherence is a reason to reach for it first.** Before adding a dependency, check the stdlib - almost always there, battle-tested, zero supply-chain risk, and since everything speaks `Reader`, `Writer`, `Context`, and `error`, it slots together without glue code.

## Recap

1. **`io.Reader` and `io.Writer`** are two one-method interfaces that nearly all I/O flows through; because files, sockets, buffers, and gzip streams all satisfy them implicitly, *any* source can be piped to *any* destination with `io.Copy` and friends.
2. **`context.Context`** threads a single cancellation/deadline signal down the entire call tree - `net/http` and database drivers all accept it, so one timeout composes across every layer. Always `defer cancel()`.
3. **`encoding/json`** maps structs to JSON via `json:"..."` struct tags; ⚠️ only *exported* (capitalized) fields are visible, and `omitempty` drops zero values too.
4. **`net/http`** is a production-grade server in a few lines - handlers use `http.ResponseWriter` (an `io.Writer`) and read `resp.Body` (an `io.Reader`), reusing the very interfaces this phase opened with.
5. **The design lesson** - `time`, `strings`, `bufio`, `slices`, and `errors` all reuse the same small-interface vocabulary. Reach for the standard library first: it's coherent, dependency-free, and composes without glue.

You now understand not just *what's* in Go's standard library, but *why* it fits together so well, which makes every new package faster to learn. Next: turning that composable code toward speed - profiling, allocation, and the optimizations that actually move the needle.

## Quick check

Test yourself on the one idea that ties this phase together - small interfaces that compose:

```quiz
[
  {
    "q": "Why can `io.Copy(dst, src)` work with a file, a network connection, and an in-memory buffer interchangeably?",
    "choices": [
      "Because each of those types satisfies the one-method io.Reader or io.Writer interface, and io.Copy only talks to those interfaces",
      "Because io.Copy has special-case code for every standard library type",
      "Because Go automatically converts all I/O types into files first",
      "Because io.Copy loads the entire source into memory before writing"
    ],
    "answer": 0,
    "explain": "io.Copy is written against io.Reader and io.Writer - the smallest interfaces it needs. Any type with the right one method satisfies them implicitly, so unrelated types like files, sockets, and buffers all plug in, including types written long after io.Copy."
  },
  {
    "q": "You marshal a struct to JSON but one field is missing from the output. The field is named `email` (lowercase). What's the most likely cause?",
    "choices": [
      "The field is unexported (lowercase), so encoding/json cannot see it",
      "JSON does not support string fields",
      "You must call json.Register on the field first",
      "Lowercase fields are always serialized as null"
    ],
    "answer": 0,
    "explain": "encoding/json lives in another package and can only access exported (capitalized) fields. A lowercase field is package-private and invisible to the marshaler. Capitalize it and use a json:\"email\" tag to control the key name."
  },
  {
    "q": "What does passing a `context.Context` with a timeout into `http.Get`, a database query, and an outbound API call give you?",
    "choices": [
      "A single cancellation signal that propagates through every layer, so one timeout stops the whole call tree",
      "Faster execution because context skips network round-trips",
      "Automatic retries of any operation that fails",
      "Encrypted communication between the layers"
    ],
    "answer": 0,
    "explain": "context.Context carries one cancellation/deadline signal across API boundaries. Because the standard library threads it consistently, a single timeout set at the top flows down and every layer watches the same Done() channel - cancellation composes for free."
  }
]
```


---

# Performance & Optimization

Here's the thing nobody tells you when you start chasing speed: most of your code is already fast enough, and most of your guesses about *where* it's slow will be wrong. Performance work is a discipline: measure, find the one place that matters, fix that, and stop. The tricks are the easy part; the discipline separates a real speedup from hours of busywork that moved nothing.

This phase caps the deep half of the guide, leaning on the runtime model from [Phase 14](14-runtime-scheduler-and-memory.md) (stack, heap, escape analysis, GC) and the tools from [Phase 15](15-testing-benchmarks-profiling.md) (`go test -bench`, `pprof`), in the order that pays off: measure, fix the algorithm, then cut allocations.

## Measure first, always

**The mental model.** Your intuition about performance is a liar - not because you're bad at this, but because modern CPUs, caches, the scheduler, and the GC interact in ways no human predicts reliably. The function you're *sure* is the bottleneck is often a rounding error, while the real cost hides in a string concatenation you never thought twice about. The only way to know is to look.

⚠️ **The number-one rule of optimization: never optimize on a hunch.** Every time you "speed something up" without measurements proving it was slow *and* that your change helped, you're gambling - and the usual prize is uglier code at the same speed. Profile first. Always.

The workflow is the one from Phase 15, used in anger:

1. Write a benchmark that exercises the real, representative work.
2. Run it under the CPU profiler to find where time actually goes.
3. Fix the single biggest cost.
4. Re-run the benchmark to *prove* the fix helped. Repeat from step 2.

```console
$ go test -bench=. -cpuprofile=cpu.prof
$ go tool pprof -top cpu.prof
Showing nodes accounting for 1.84s, 92.0% of 2.00s total
      flat  flat%   sum%        cum   cum%
     1.20s 60.0%  60.0%      1.20s 60.0%  main.findDuplicates
     0.40s 20.0%  80.0%      0.40s 20.0%  runtime.mapassign_faststr
     0.24s 12.0%  92.0%      0.24s 12.0%  runtime.mallocgc
```
*What just happened:* `pprof -top` ranked functions by CPU time burned. Sixty percent sits in one function, `findDuplicates` - your hot spot, nothing else worth touching until it's handled. The `mallocgc` line (allocation) is a hint we'll come back to. (Numbers vary by machine; the *shape* - one function dominating - is typical.)

💡 **Key insight.** In almost every program, a tiny fraction of the code accounts for the overwhelming majority of runtime. Your job is to find that 3% and leave the other 97% alone, readable and untouched. Profiling finds the 3%; optimizing the rest only adds risk.

## Algorithmic cost dominates

**The mental model.** Before fiddling with a single allocation, ask: *is the approach itself right?* The largest wins in practice almost never come from micro-tweaks - they come from replacing an expensive strategy with a cheaper one, turning an O(n²) nested scan into an O(n) pass with a map. No low-level cleverness rescues a quadratic algorithm; it just delays the cliff.

If "O(n²)" and "O(n)" feel fuzzy, the dedicated primer [Big-O Without the Math Panic](/guides/big-o-without-the-math-panic) walks through exactly what they mean and why they decide who wins as your data grows.

Here's the classic: finding items in a slice that appear more than once. The naive version compares every element against every other:

```go
// O(n²): for each item, scan all the others looking for a match.
func findDuplicatesSlow(items []string) []string {
	var dups []string
	for i := 0; i < len(items); i++ {
		for j := i + 1; j < len(items); j++ {
			if items[i] == items[j] {
				dups = append(dups, items[i])
				break
			}
		}
	}
	return dups
}
```

The map version makes one pass, remembering what it has seen:

```go
// O(n): one pass, a map remembers what we've already seen.
func findDuplicatesFast(items []string) []string {
	seen := make(map[string]bool, len(items))
	var dups []string
	for _, item := range items {
		if seen[item] {
			dups = append(dups, item)
		}
		seen[item] = true
	}
	return dups
}
```

*What just happened:* both answer the same question, but the cost curves aren't alike. The slow version's inner loop grows with the *square* of the input - double the items, quadruple the comparisons. The fast version trades a little memory (the `seen` map) for a single linear pass: a map lookup is roughly constant-time, so doubling the input only doubles the work. On 10 items the difference is invisible; on 100,000 it's instant versus a coffee break.

Benchmarked side by side, the gap is brutal:

```console
$ go test -bench=Duplicates -benchmem
BenchmarkDuplicatesSlow-8        37    31_847_201 ns/op      analysis on 10k items
BenchmarkDuplicatesFast-8     5_142       233_004 ns/op      analysis on 10k items
```
*What just happened:* at 10,000 items the map-based version is over a hundred times faster (`ns/op` = nanoseconds per operation, lower is better), and the multiplier *grows* with input size: at 100,000 the quadratic version is thousands of times slower. Algorithm choice dwarfs everything else - you cannot micro-optimize your way out of the wrong complexity class.

Play with how each growth curve behaves as `n` climbs - it makes the O(n²)-vs-O(n) gap concrete in a way numbers on a page can't:

```playground-bigo
```

## Allocations are the usual Go bottleneck

**The mental model.** Once your algorithm is sound, the most common remaining drag in Go is *heap allocation*. Recall [Phase 14](14-runtime-scheduler-and-memory.md): values escaping to the heap cost more than stack values, and every heap allocation is something the GC must later track and reclaim. More allocations means more GC work stealing CPU. So "make it faster" often means "make it allocate less."

📝 **`allocs/op`** - the average number of distinct heap allocations one run of a benchmarked operation makes, seen via `-benchmem`. Often a *better* optimization target than raw time, since cutting allocations cuts GC pressure, lowering time *and* steadying performance under load.

The single most common waste: growing a slice from nothing when you already know how big it'll get. Each time `append` runs out of capacity it allocates a *new, larger* backing array and copies everything over, so a slice built one element at a time can allocate many times over its life.

```go
// Wasteful: starts empty, reallocates the backing array as it grows.
func squaresGrowing(n int) []int {
	var out []int
	for i := 0; i < n; i++ {
		out = append(out, i*i) // may reallocate + copy repeatedly
	}
	return out
}

// Lean: one allocation, exactly the right size, up front.
func squaresPrealloc(n int) []int {
	out := make([]int, 0, n) // length 0, capacity n - room reserved
	for i := 0; i < n; i++ {
		out = append(out, i*i) // never reallocates; capacity already there
	}
	return out
}
```

*What just happened:* `squaresGrowing` starts with a `nil` slice and lets `append` discover the size the hard way, grabbing a bigger array and copying old contents in each time capacity runs out. `squaresPrealloc` calls `make([]int, 0, n)`: capacity `n` reserved immediately, so every `append` drops into space that already exists - the whole slice costs exactly *one* allocation. Same output, a fraction of the garbage.

```console
$ go test -bench=Squares -benchmem
BenchmarkSquaresGrowing-8     291_204   4_071 ns/op   16_376 B/op   12 allocs/op
BenchmarkSquaresPrealloc-8    876_553   1_355 ns/op    8_192 B/op    1 allocs/op
```
*What just happened:* `-benchmem` added two columns: `B/op` (bytes allocated per operation) and `allocs/op` (count of allocations). The growing version made 12 separate allocations and churned twice the memory; the preallocated version made exactly 1 - roughly a 3x speedup from one trivially small change.

Two other everyday spots for the same principle:

- **Reuse buffers instead of re-creating them.** Building strings with `+` in a loop allocates a fresh string every concatenation; a `strings.Builder` (or reused `[]byte`) writes into one growing buffer.
- **Avoid needless boxing into `interface{}` or pointers.** Stuffing a value into an empty interface, or passing its address around, is exactly what makes it *escape to the heap* (Phase 14). In a hot loop, passing values directly keeps them on the stack - free to create, nothing for the GC to chase. Let the profiler and `go build -gcflags=-m` tell you what's escaping.

## `sync.Pool` - recycle short-lived temporaries

**The mental model.** Sometimes a hot path *must* allocate a chunky temporary over and over - a scratch buffer, a parser's work area - and each allocation is GC pressure. `sync.Pool` is Go's tool: a free list of already-allocated objects you borrow and return, so a few objects get reused across thousands of operations instead of allocating fresh each time.

📝 **`sync.Pool`** - a concurrency-safe pool of reusable, temporary objects. `Get()` returns one (creating it via `New` only if empty); `Put()` hands it back for the next caller. The point: recycle fungible temporaries and slash allocation churn in hot paths.

```go
package main

import (
	"bytes"
	"fmt"
	"sync"
)

// Pool of reusable byte buffers. New runs only when the pool is empty.
var bufPool = sync.Pool{
	New: func() any { return new(bytes.Buffer) },
}

func render(msg string) string {
	buf := bufPool.Get().(*bytes.Buffer) // borrow (type-assert back to *Buffer)
	defer func() {
		buf.Reset()      // wipe contents so the next borrower starts clean
		bufPool.Put(buf) // return it for reuse
	}()

	buf.WriteString("[log] ")
	buf.WriteString(msg)
	return buf.String()
}

func main() {
	fmt.Println(render("started"))
	fmt.Println(render("done"))
}
```
```console
$ go run main.go
[log] started
[log] done
```
*What just happened:* the first `render` call found the pool empty, so `New` made a fresh `bytes.Buffer`. On the way out, `Reset()` cleared it and `Put()` returned it; the *second* call's `Get()` handed back that same buffer instead of allocating a new one. Across a hot path doing this millions of times, you allocate a handful of buffers total - a large cut in GC work. Note `Reset()`: a pooled object carries whatever the last borrower left in it, so clear it before reuse.

⚠️ **`sync.Pool` is not a cache, and doesn't keep your objects alive.** The GC can empty the pool at any collection - an object you `Put` may be gone the next `Get` (`New` just makes a new one, no harm done). Fine for *fungible temporaries*, completely wrong for anything you need to persist, with identity, or expensive to lose. Use it only to relieve measured allocation pressure on interchangeable short-lived objects.

## Knowing when to stop

**The mental model.** Optimization has a point of diminishing - then *negative* - returns. Every clever rewrite makes code harder to read, change, and break, a cost paid by every future reader. The goal is never "as fast as physically possible" - it's "fast enough, and no more twisted than it has to be."

The discipline:

- **Optimize the measured hot path. Leave the rest clear.** The 3% that profiling flagged earns the right to be clever; the other 97% stays as straightforward as possible.
- **Define "fast enough" up front, then stop when you hit it.** A target ("p99 under 50ms") tells you when you're done. Without one, optimization never ends.
- **Re-measure after every change.** A change that doesn't move the benchmark isn't an optimization - revert it.

💡 **The closing rule of the deep half.** Readable code that's fast enough beats clever code that's unmaintainable, every time. Measure, fix the algorithm, cut the allocations that matter, and then - the hardest part - stop.

## Recap

1. **Measure first, always.** Never optimize on a hunch. Use benchmarks plus `pprof` to find the real hot spot; most code is already fast enough, so hunt the 3% that isn't.
2. **Algorithmic cost dominates.** The biggest wins come from a better approach (O(n) map lookup over an O(n²) nested scan), not micro-tweaks - you can't optimize your way out of the wrong complexity class.
3. **Allocations are the usual Go bottleneck.** Fewer heap allocations means less GC pressure means faster, steadier code. Preallocate with `make([]T, 0, n)`, reuse buffers, and avoid needless boxing that escapes to the heap; watch `allocs/op` with `-benchmem`.
4. **`sync.Pool` recycles fungible temporaries** in hot paths to cut allocation churn - but it's not a cache; the GC can empty it anytime, so use it only for interchangeable short-lived objects, and `Reset` before reuse.
5. **Know when to stop.** Optimize the measured hot path and leave the rest clear; define "fast enough" up front, re-measure after every change, and revert anything that didn't move the number.

That's the deep half done. You can now reason about how Go runs your code *and* make it faster on purpose, with evidence instead of guesses. The final phase steps back: where Go genuinely shines, and where to point yourself next.

## Quick check

Test yourself on the discipline that makes performance work actually pay off:

```quiz
[
  {
    "q": "Before changing any code to make a Go program faster, what should you do first?",
    "choices": [
      "Profile with benchmarks and pprof to find where time actually goes",
      "Add sync.Pool everywhere objects are created",
      "Rewrite the slowest-looking function from memory",
      "Switch every slice to a preallocated fixed size"
    ],
    "answer": 0,
    "explain": "Intuition about bottlenecks is unreliable. Measure first with benchmarks and pprof so you optimize the real hot spot - the small fraction of code that actually dominates runtime - instead of guessing."
  },
  {
    "q": "You replace a slow function and want to know if it mattered. Which single change usually delivers the biggest speedup on large inputs?",
    "choices": [
      "Choosing a better algorithm - e.g. an O(n) map lookup instead of an O(n²) nested scan",
      "Renaming variables so the compiler optimizes better",
      "Adding more goroutines to the inner loop",
      "Removing all comments from the hot path"
    ],
    "answer": 0,
    "explain": "Algorithmic complexity dominates. Turning a quadratic approach into a linear one wins by a margin that grows with the input - no micro-optimization can rescue the wrong complexity class."
  },
  {
    "q": "Why is sync.Pool wrong for storing objects you need to keep around?",
    "choices": [
      "The garbage collector can empty the pool at any GC, so a pooled object may vanish",
      "sync.Pool is not safe for concurrent use",
      "Objects in a pool are deep-copied, doubling memory use",
      "Get() always allocates a brand-new object, defeating the purpose"
    ],
    "answer": 0,
    "explain": "sync.Pool is a free list for fungible temporaries, not a cache. The GC can clear it during any collection, so anything you Put may be gone on the next Get. It's only safe for interchangeable short-lived objects where losing one is harmless."
  }
]
```


---

# Where to Go Next

You've covered the whole language: install and syntax, collections and control flow, modules, goroutines and channels, errors and I/O, the toolchain, and the idioms - not a beginner's slice of Go, that's *Go*. The remaining question: now what do you build?

This phase is deliberately short and to the point. Go isn't equally good at everything, so the kindest thing I can do is point you at where it genuinely shines, name the few libraries you'll actually reach for, and get out of your way.

## Where Go actually shines

**Web services and APIs.** Go's daily-driver job. The standard library's `net/http` (a full server in [Phase 8](08-ecosystem-and-tooling.md)) is genuinely production-ready, and many teams ship real services on it alone. For nicer routing and middleware, the common choices are **chi** (thin, idiomatic, close to the standard library) and **gin** (heavier, batteries-included). Start with the standard library, add a router only when you feel the friction.

**Command-line tools.** Go compiles to a single static binary with no runtime to install ([Phase 8](08-ecosystem-and-tooling.md)), close to ideal for CLIs. For anything beyond a couple of flags, **cobra** is the de facto library for commands and subcommands (it powers `kubectl` and `gh`). Many developer tools you already use are Go binaries for exactly this reason.

**Cloud and infrastructure - Go's home turf.** Here Go isn't just *usable* but *dominant*. **Docker** and **Kubernetes** are both written in Go, along with a large slice of the cloud-native ecosystem - Terraform, Prometheus, etcd. Not an accident: fast compiles, single static binaries, first-class concurrency ([Phase 6](06-goroutines-and-channels.md)), and a strong networking library are exactly what infrastructure software needs.

```mermaid
flowchart LR
  Go(Go) --> Web[Web services<br/>net/http · chi · gin]
  Go --> CLI[CLI tools<br/>cobra]
  Go --> Infra[Cloud & infra<br/>Docker · Kubernetes]
```

📝 **Terminology.** *Cloud-native* describes software built to run in containers and orchestrators (like Kubernetes) rather than on a single fixed server. A surprising amount of it is Go - so knowing Go opens that world of tooling to you.

## What to build next

Pick one and finish it. A small thing you complete teaches you more than an ambitious thing you abandon.

- **A JSON API.** A tiny HTTP service with two or three endpoints backed by an in-memory map, using `net/http` and `encoding/json`. Add **chi** when the routing starts to chafe.
- **A real CLI.** Take a chore you do by hand - renaming files, summarizing a log, checking a list of URLs - and make it a command-line tool. Start with `flag`; graduate to **cobra** for subcommands.
- **A concurrent fetcher.** Fetch a list of URLs at once with goroutines and a `sync.WaitGroup` ([Phase 6](06-goroutines-and-channels.md)), collecting results over a channel - it makes Go's concurrency model concrete in a way no tutorial can.

For each, the loop is the same: write it, run `go fmt`, `go vet ./...`, and `go test ./...` ([Phase 8](08-ecosystem-and-tooling.md)), reaching for the standard library before any third-party package.

## Where to go from here

The official **A Tour of Go** (go.dev/tour) and **Effective Go** (go.dev) are the two resources worth bookmarking - maintained by the Go team. And if you want to think about *why* languages make the choices they do - static binaries and goroutines versus a VM and threads - that's the subject of [Languages, Explained Like a Human](/guides/languages-explained-like-a-human).

You came in not knowing Go. You're leaving able to read it, write it idiomatically, handle concurrency and errors without fear, and reach for the right tool from the box. That transfers - the cloud-native world runs on it. Go build the small thing. You're ready.

## Recap

1. **Web** - `net/http` is production-ready; add **chi** (light, idiomatic) or **gin** (heavier, batteries-included) when you want a router.
2. **CLIs** - single static binaries make Go great for tools; **cobra** handles commands and subcommands.
3. **Cloud & infra** - Go's home turf; Docker and Kubernetes are written in Go, and much of the cloud-native stack with them.
4. **Build one real thing** - a JSON API, a CLI, or a concurrent fetcher - and finish it, leaning on the standard library and the toolchain.
5. **Next reading** - A Tour of Go and Effective Go for depth; [Languages, Explained Like a Human](/guides/languages-explained-like-a-human) for the bigger picture.
