skip to content

You launch three goroutines to fetch the same file from three mirrors. How does the caller return as soon as the fastest one answers?

level: juniorimportance: should knowfreq 48%

answer

  1. many senders, one receiver
  2. you never wait for all of them
  3. starting order decides nothing
  4. a single receive is the whole selection
  5. the losers keep running after you return

basics

~20 s

Give every attempt the same result channel and receive from it exactly once. That single receive yields the first value any goroutine sends, so the caller returns with it immediately and never waits for the other two attempts to finish.

solid answer

~40 s

Each attempt runs in its own goroutine and sends its outcome to one shared result channel; the caller does a single `r := <-ch` and returns. A receive takes whichever value arrives first, so the fastest mirror decides the answer — there is no `WaitGroup`, no ranging over the channel, and no ordering by which goroutine was started. Two things go with it: make the channel buffered with one slot per attempt so the losers can still send and exit after nobody is receiving, and derive a cancellable `context.Context` for the attempts and cancel it on return so the abandoned fetches stop early. The losing goroutines keep running until they notice that cancellation; returning from the caller does not stop them.

code

go · 21 lines
go
type result struct {
	mirror string
	body   []byte
	err    error
}

func fetchFastest(ctx context.Context, mirrors []string, name string) ([]byte, error) {
	ctx, cancel := context.WithCancel(ctx)
	defer cancel() // tell the losers to stop once we return

	ch := make(chan result, len(mirrors)) // one slot per sender
	for _, m := range mirrors {
		go func() {
			b, err := fetch(ctx, m, name)
			ch <- result{mirror: m, body: b, err: err}
		}()
	}

	r := <-ch // the first value to arrive decides the answer
	return r.body, r.err
}

go deeper

for a junior

Be ready to write it: a goroutine per attempt, one shared result channel, and a single receive that returns the first value to arrive. Know that you do not wait for the rest and that arrival order, not start order, decides the winner.

for a middle

Explain why a single receive is the entire selection mechanism, and what state the abandoned goroutines are left in — still running, still holding sockets, and still needing somewhere to put their value.

for a senior

Show that you treat the losers as live code: bound their cost with a shared cancellable context, keep their writes off anything the caller owns, and record per-attempt outcomes so you can tell whether the race is earning its duplicate load.

for a principal

Frame it as a latency-for-work trade you have to justify: racing N mirrors multiplies egress and upstream load to cut tail latency, and it is only sound where attempts are genuinely idempotent and redundant.

## What the pattern is **First result wins** is what you write when several independent attempts can each produce the same answer and you only care about whichever finishes first. The worked example here is a package-download client that can pull the same checksum-verified archive from three mirrors: any mirror will do, they differ only in speed and availability, so you ask all three at once and keep the first archive that arrives. This is a race you *want*. It trades duplicate work for latency: you pay three downloads to get the p99 of the fastest mirror instead of the p99 of whichever mirror you happened to pick. ## The mechanism Three pieces, and each one matters: 1. **One goroutine per attempt.** Each attempt is started with `go`, and each one sends exactly one value describing its outcome — bytes plus error — to a shared channel. 2. **One shared result channel.** Every attempt is a sender; the caller is the only receiver. 3. **Exactly one receive.** `r := <-ch` blocks until *some* goroutine sends. A channel receive does not care which goroutine sends, or in what order they were started; it hands you the first value that becomes available. That single receive *is* the selection logic. There is nothing else to write. So the caller's body is: start N goroutines, receive once, return. Compare that with the shape juniors reach for by reflex — a `sync.WaitGroup`, `Wait()`, then look at the results. That waits for the *slowest* attempt, which is the exact opposite of what a race is for. ## What happens to the losers Returning from the calling function does **not** stop the other goroutines. Go has no way to kill a goroutine from outside; a goroutine ends only when its own function returns. So after the caller has returned, two downloads are still in flight, still holding sockets and still filling buffers. Two consequences follow, and both are part of writing the pattern correctly: - **They must be able to finish their send.** If the result channel is unbuffered, a losing goroutine's `ch <- result{...}` blocks forever, because the only receiver is gone. That goroutine, its stack and everything it references are pinned for the life of the process. Give the channel one slot per attempt (`make(chan result, len(mirrors))`) so every send completes whether anyone is listening or not. - **They should be told to stop.** Derive a cancellable context from the caller's, hand the same one to all attempts, and `defer cancel()`. When the caller returns, the in-flight HTTP requests for the losing mirrors are aborted rather than downloading a whole archive nobody will read. ## Why a single receive is enough — and when it is not A plain single receive returns the first attempt to *finish*, which is not the same as the first attempt to *succeed*. A mirror that is down and refuses the connection in two milliseconds will win the race and hand you its error while the two healthy mirrors are still downloading. When the attempts can fail, the caller receives in a loop instead: return on the first result whose error is nil, remember the others, and only give up once all N have reported. ## Idiom notes - Model the outcome as one struct (`type result struct { body []byte; err error }`) and send that. Two parallel channels — one for values, one for errors — turn the single receive into a `select` and buy nothing. - The receive gives you no information about *which* attempt won unless you put that in the struct. Carrying the mirror name in the result is what makes the attempt-outcome log useful later: you can see which candidate won, how far behind the losers were, and whether one mirror never wins at all. - Do not let the attempts write into memory the caller owns (a shared slice, a struct field, a destination file). The losers run past the caller's return, so any such write is a write after return — a data race, and with a file, a corrupt artifact. Each attempt writes to its own buffer or its own temporary file; only the winner's is used. - The pattern is worth it when attempts are genuinely redundant and the duplicate cost is acceptable. Racing three writes, three payments or three anything with side effects is not this pattern.

  • Why not use a sync.WaitGroup here and inspect the results afterwards?
    `WaitGroup.Wait` returns when the *last* attempt finishes, so the call takes as long as the slowest mirror — the opposite of what racing them is for. A `WaitGroup` is the right tool when you need every result; when you need only the first, the receive itself does the waiting and the selecting.
  • Does returning from the calling function stop the two losing goroutines?
    No. Nothing in Go can stop a goroutine from outside; it ends only when its own function returns. The losers keep downloading until they finish or until they observe cancellation on the context you shared with them, which is why the pattern derives a cancellable context and cancels it on the way out.
  • How do you learn which mirror actually won?
    Put the identity in the value you send — a `mirror` field on the result struct — because a channel receive tells you nothing about its sender. Logging that field, plus each loser's finish time as it arrives, is what lets you see whether one mirror ever wins and whether the race is buying you anything.

saying these in an interview costs you the question

  • Uses a WaitGroup and waits for every attempt to finish
  • Assumes the first goroutine started is the first to send
  • Ranges over the result channel instead of receiving once
  • Believes returning from the caller kills the other goroutines
  • Has all attempts append into one shared caller-owned slice