Files
go-kata/17-context-aware-channel-sender
2026-01-04 20:40:07 +01:00
..
2026-01-04 20:40:07 +01:00

Kata 17: The Context-Aware Channel Sender (No Leaked Producers)

Target Idioms: Pipeline Cancellation, Select-on-Send, Channel Ownership, Goroutine Leak Prevention
Difficulty: 🟡 Intermediate

🧠 The "Why"

A goroutine sending on a channel blocks until a receiver is ready (unless buffered space is available). If the receiver exits early (timeout, HTTP cancel, upstream error), producers can block forever and leak.

Idiomatic Go fixes this by:

  • threading context.Context through the pipeline
  • selecting on every send (case out <- v vs case <-ctx.Done()), as recommended in Gos pipeline cancellation patterns and real leak writeups.

🎯 The Scenario

Youre building a data pipeline step that fetches N URLs concurrently and streams results downstream. If the request is canceled (client disconnect, global timeout), all fetchers must stop immediately and no goroutine may remain blocked on out <- result.

🛠 The Challenge

Implement:

  • type DataFetcher struct { ... }
  • func (f *DataFetcher) Fetch(ctx context.Context, urls []string) <-chan Result

Where:

  • Result contains URL, Body []byte, Err error (or similar).

1. Functional Requirements

  • Start concurrent fetchers for all URLs (or bounded concurrency if you choose).
  • Send results as they complete (order doesnt matter).
  • Stop promptly on ctx.Done().
  • Close the output channel exactly once after all producers exit.
  • Return partial results that already completed before cancellation.

2. The "Idiomatic" Constraints (Pass/Fail Criteria)

  • Every send uses select: no bare out <- x.
  • Channel ownership: only the producer side closes out.
  • No goroutine leaks: all goroutines exit when ctx is canceled.
  • No double close: prove it structurally (single closer goroutine). Avoid sync.Once unless you can justify it.
  • Buffer choice is intentional: if you buffer, document why and how you chose the size.

3. Hints (Allowed Tools)

  • You may use errgroup or a simple worker pattern, but the key is: send must be cancel-aware.
  • If you do bounded concurrency, prefer x/sync/semaphore or a worker pool (but dont turn this kata into a rate-limiter kata).

🧪 Self-Correction (Test Yourself)

  1. Forgotten Sender

    • Start 50 fetchers, consume only 1 result, then cancel.
    • Pass: goroutine count returns near baseline quickly (use runtime.NumGoroutine() as a sanity check).
  2. Cancellation Before First Receive

    • Cancel ctx immediately after calling Fetch.
    • Pass: no goroutine blocks trying to send.
  3. Close Discipline

    • Cancel ctx from multiple places.
    • Pass: no panic: close of closed channel.

📚 Resources