// lesson: pipeline-surgery

Pipeline Surgery

Pipelines compose when every stage honors the same contract: consume until the input closes, close your outputs when you're done, never strand a goroutine. Two plumbing fittings show up constantly:

  • Tee splits one stream into two, like the shell command โ€” every value goes to both outputs.
  • Bridge flattens a channel of channels (<-chan <-chan T) into one stream (<-chan T): read the next inner channel off the outer one, drain it completely, then move to the next. It shows up when a producer hands you a fresh channel per unit of work โ€” pagination, one channel per shard โ€” and consumers just want one linear stream instead of juggling N.

This lesson's challenge below is Tee only; Bridge is here so the name isn't a surprise if you meet it in the wild.

Tee has a sharp edge: you must send each value to both outputs, but the two consumers run at different speeds, and you can't just send to one then the other โ€” if the first consumer stalls, the second starves even though it's ready. The idiomatic move is the nil-channel trick: in a loop, select over both sends, and after a send succeeds set that channel variable to nil so its case goes dormant until the next value:

o1, o2 := out1, out2
for i := 0; i < 2; i++ {
	select {
	case o1 <- v:
		o1 = nil
	case o2 <- v:
		o2 = nil
	}
}

Note the honest consequence: tee advances one value at a time, so the fast consumer can run at most one value ahead of the slow one. That's correct โ€” it's backpressure, not a bug. What is a bug: forgetting to close both outputs, or leaving the forwarding goroutine alive after the input closes.

โ€บ Tee

35 pts

Implement:

func Tee(in <-chan int) (<-chan int, <-chan int)

Required semantics:

  • Every value received from in is delivered to both outputs, each in the original input order.
  • It must work with the two consumers running at different speeds, as long as both are actively consumed. (Lockstep coupling โ€” the fast output waiting at most one value for the slow one โ€” is expected and fine.)
  • When in closes, both outputs close after delivering everything, including the zero-value case where in closes immediately.
  • No leaks: after a full tee cycle completes, every goroutine you started must have exited. The tests run several cycles and compare runtime.NumGoroutine() before and after with settling time.

Log in to submit a solution and earn points.