# 🔀 Goroutines, Channels, and Context for Request-Scoped Work

> Sign in to view the full content of this lesson.

## Learning Objectives

- Explain what a goroutine actually costs and what guarantees it does *not* give you.
- Pick channels, `sync.Mutex`, or `errgroup` for a specific fan-out shape, and say why the other two are wrong there.
- Propagate deadlines and cancellation through a call tree with `context`.
- Spot the three leak patterns that show up most often in request handlers.

## Quick Review

Last lesson we established that wrapped errors plus `errors.Is`/`errors.As` keep failure information intact on the way up. That matters here immediately: cancellation surfaces as an error value, and you compare it with `errors.Is(err, context.DeadlineExceeded)`, not with string matching. Value semantics also come back: a `context.Context` is an interface value you pass down, not a field you copy into your struct.

## Theory

### Goroutines are cheap, not free

A goroutine starts with a couple of kilobytes of stack that grows on demand. Spawning one per request or per downstream call is normal in Go. What you do not get for free is an exit path: a goroutine has no return value, no error channel, and no built-in way for anyone to wait for it. If it blocks forever, it is still there, holding its stack and whatever it captured. That is a leak, and leaks kill long-running services slowly.

Two more facts worth internalizing: a panic inside a goroutine that nobody recovers takes down the whole process, and `main` returning kills every goroutine without waiting.

### Channels are for handoff, not for queues by default

An unbuffered channel is a rendezvous: the send blocks until a receiver is ready. A buffered channel of size N lets the sender run ahead by N values before it blocks. Buffers are a decoupling tool, and only sometimes a leak fix.

Channel rules that bite people:

- Only the sender closes a channel, and only when it owns the send side. Sending on a {{c4::closed}} channel panics; receiving from it returns the zero value immediately with `ok == false`.
- Channels have direction in signatures (`chan<- T`, `<-chan T`). Use it, the compiler will then catch half your mistakes.
- `select` with a `ctx.Done()` case is how a blocked goroutine bails out.

> [!WARNING]
> A goroutine stuck forever on an unbuffered send with no receiver is a {{c1::goroutine leak}}. A buffer of `len(ids)` plus a `select` on `ctx.Done()` is the usual fix.

### Channel or mutex?

Rule of thumb: channels move data and express ownership transfer; mutexes protect state that stays put. If all you need is a counter or a map guarded against concurrent writes, `sync.Mutex` is shorter, faster to read, and impossible to leak. Reach for channels when there is a pipeline, a fan-out/fan-in, or a lifecycle an owner must coordinate.

```mermaid
flowchart TD
    A["Fan-out work to do"] --> B{"Moving data or guarding state?"}
    B -->|"Guarding shared state"| C["sync.Mutex"]
    B -->|"Moving data"| D{"Need every result plus the first error?"}
    D -->|"Yes"| E["errgroup.WithContext"]
    D -->|"No, request-scoped work"| F["channels plus select on ctx.Done"]
```

### Context is the request's lifeline

`context.Context` carries a deadline, a cancellation signal, and a small bag of request-scoped values. Conventions that are effectively law:

- Pass it as the {{c3::first}} parameter, named `ctx`. Never store it in a struct.
- Every `context.WithTimeout` or `WithCancel` returns a cancel func you must {{c2::defer}}: skipping it leaks the timer until the deadline fires.
- `ctx.Err()` tells you *why* you stopped (`context.Canceled` or `context.DeadlineExceeded`).
- A child deadline is always the minimum of its own and its parent's. Setting a timeout deep inside the stack is usually a bug; set it at the edge (the handler) and let it flow down.

> [!NOTE]
> `errgroup.WithContext` is `sync.WaitGroup` plus two things: it returns the {{c5::first non-nil}} error, and it cancels the derived context as soon as any goroutine fails. We build the real fan-out worker with it next lesson; for now just know it replaces most hand-rolled "collect N results" channel code.

## Worked Examples

### Example 1: the smallest correct handoff

```go
func main() {
    ch := make(chan int) // unbuffered

    go func() {
        ch <- 42 // blocks until someone receives
    }()

    fmt.Println(<-ch) // 42, and the send unblocks
}
```

Walk it through: `make(chan int)` creates a rendezvous point. The goroutine reaches the send and parks. The main goroutine reaches the receive, the two pair up, the value moves, and the spawned goroutine can exit. If `main` had returned before the receive, the goroutine would be killed silently, no leak, but also no result. That asymmetry is why "fire and forget" needs an explicit owner.

### Example 2: a leak, then the fix

```go
// Leaky: the losing goroutines block on ch <- res forever.
func firstResult(ctx context.Context, ids []string) (string, error) {
    ch := make(chan string)
    for _, id := range ids {
        go func(id string) {
            if res, err := load(ctx, id); err == nil {
                ch <- res
            }
        }(id)
    }
    select {
    case r := <-ch:
        return r, nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}
```

Only one value is ever received, so every other happy-path goroutine parks on the send forever. Two changes fix it: buffer the channel to `len(ids)` and make the send ctx-aware.

```go
ch := make(chan string, len(ids))
// inside the goroutine:
select {
case ch <- res:
case <-ctx.Done():
}
```

Now a sender either lands in the buffer or gives up when the request dies. Nothing is left blocked.

### Example 3: deadline at the edge, cancellation downward

```go
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
    defer cancel()

    user, err := h.store.UserByID(ctx, r.URL.Query().Get("id"))
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            http.Error(w, "upstream timeout", http.StatusGatewayTimeout)
            return
        }
        http.Error(w, "internal error", http.StatusInternalServerError)
        return
    }
    _ = json.NewEncoder(w).Encode(user)
}
```

The 2 second budget is set once, at the boundary. `UserByID` receives it and hands it to the driver, so a slow query is cancelled rather than left running after the client has gone. Note `r.Context()` rather than `context.Background()`: the request context already cancels when the client disconnects.

## Discussion & Reflection

1. Your handler needs to fetch three independent resources and return only the first successful one. Which primitive do you reach for, and what happens to the other two goroutines?
2. Where would you put the timeout for a call chain of handler, service, repository, and database? What breaks if every layer sets its own?
3. You have a `map[string]int` cache written from several goroutines. Argue both sides of "channels only" versus a mutex, then pick.

## Expert Corner

- Set `-race` in your test command and keep it there. Data races in request-scoped code are invisible until production load finds them.
- `runtime.NumGoroutine()` in a test can confirm a leak; `go.uber.org/goleak` does it systematically.
- Bound the fan-out. Spawning one goroutine per item of an unbounded slice is a self-inflicted outage; a semaphore channel of capacity K keeps concurrency predictable.
- Fire-and-forget work that must outlive the request (audit logs, metrics flush) should deliberately detach with `context.WithoutCancel(ctx)`. Do it consciously, never by passing `context.Background()` into a deep call.
- Values in context are for request-scoped metadata like trace or request IDs, with unexported key types. If it is a dependency, wire it explicitly.
- Cancellation is cooperative: a goroutine that never checks `ctx` ignores it completely. Every blocking operation in the chain must accept and honor the context.

## Summary & Next Steps

Goroutines are cheap but need an exit path. Channels express handoff and ownership, mutexes guard state that stays put, and context carries the deadline and cancellation that make request-scoped work terminate. Buffers plus `select` on `ctx.Done()` are how you stop leaking; `defer cancel()` and edge-set deadlines are how you stop waiting.

Next lesson we take these pieces and build the real thing: a bounded concurrent fan-out worker using `errgroup.WithContext`, a semaphore, and a first-error-wins policy, wired into a full worked project.

```flashcard
[
  { "front": "What does `go f()` give you back?", "back": "Nothing: no return value, no error, no handle. Results and errors must be passed out explicitly." },
  { "front": "What is the initial cost of a goroutine?", "back": "A small stack, a couple of kilobytes, that grows on demand. Cheap enough to spawn per request." },
  { "front": "How does an unbuffered channel send behave?", "back": "It blocks until a receiver is ready, a direct handoff." },
  { "front": "When does a sender block on a buffered channel of size N?", "back": "Only when the buffer is full." },
  { "front": "Who is allowed to close a channel?", "back": "The sender, and only when it owns the send side. Receivers never close." },
  { "front": "What happens when you receive from a closed channel?", "back": "You get the zero value immediately, with ok == false." },
  { "front": "What happens when you send on a closed channel?", "back": "The program panics." },
  { "front": "How does a blocked goroutine escape cancellation?", "back": "A select with a `case <-ctx.Done():` branch." },
  { "front": "Which context constructor requires a deferred cancel?", "back": "context.WithTimeout and context.WithCancel, otherwise the timer or child is leaked." },
  { "front": "Where should a context.Context live in an API?", "back": "As the first parameter named ctx, never stored in a struct." },
  { "front": "When is a mutex the better choice over a channel?", "back": "When guarding small shared state (counter, map) that stays put and no ownership is transferred." },
  { "front": "What does errgroup.Group add over sync.WaitGroup?", "back": "It returns the first non-nil error and cancels the derived context on first failure." },
  { "front": "What does ctx.Err() return and why care?", "back": "context.Canceled or context.DeadlineExceeded, so you can distinguish why work stopped." },
  { "front": "Why prefer r.Context() over context.Background() in a handler?", "back": "The request context already cancels when the client disconnects or the deadline passes." }
]
```
