Files
kacerrandClaude Fable 5 7219eb5a23 Initial commit: Go SRE interview exercises
Includes solved 02-worker-pool and 09-safe-counter (mutex, atomic, sharded).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbnJj9ZqEx9nUvzkjdwfMq
2026-08-28 22:18:09 +02:00

2.4 KiB

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:

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

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:

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.