# 01 — Concurrent URL Health Checker The single most frequently asked Go task in SRE interviews. It's a compressed version of your actual job: blackbox probing with bounded fan-out. ## The two idioms for bounding concurrency **Semaphore** — a buffered channel where the *capacity* is the limit: ```go sem := make(chan struct{}, limit) sem <- struct{}{} // acquire (blocks when full) <-sem // release ``` **Worker pool** — `limit` goroutines ranging over a jobs channel. Semaphore is less code when each job is independent; worker pool wins when workers carry state (a connection, a buffer). Be able to write both and *say why you chose one*. ## Why `results[i]` needs no mutex Each goroutine writes to a distinct index. Different elements of a slice are distinct memory locations, so there's no data race — the race detector agrees. The `wg.Wait()` creates the happens-before edge that makes all those writes visible to the reader. This trick ("index-addressed results") deletes an entire class of locking from your solution. Interviewers notice. ## The body-drain ritual and why it exists ```go io.Copy(io.Discard, resp.Body) resp.Body.Close() ``` `http.Transport` only returns a connection to the keep-alive pool if the body was fully read. Close-without-drain forces the connection to be torn down → new TCP+TLS handshake per request → your "health checker" DDoSes your own handshake path. Real incidents have been caused by exactly this. ## Things people get wrong here - **`http.DefaultClient` has no timeout.** None. A hung server hangs your checker forever. Always construct your own client, or use `http.NewRequestWithContext`. - **A 500 response is not an `error`.** Transport errors (DNS, conn refused, timeout) populate `err`; HTTP status is data. Conflating them is a classic monitoring bug (your alerting says "down" when it's actually "up and angry"). - **Goroutine leak on cancellation**: if you acquire the semaphore with a bare `sem <- struct{}{}` and ctx dies, goroutines queue up forever. Acquire inside a `select` with `ctx.Done()`. ## Level-up flex `golang.org/x/sync/errgroup` collapses the whole choreography: ```go g, ctx := errgroup.WithContext(ctx) g.SetLimit(limit) for i, u := range urls { g.Go(func() error { results[i] = check(ctx, u); return nil }) } g.Wait() ``` Mention it exists, then implement the raw version anyway — they want to see you handle the primitives.