Implement a concurrent prime sieve with a daisy chain of goroutines. How many goroutines does it create, and how do you stop it without leaks?
A generator emits 2, 3, 4, ... For every prime p it finds, main adds a filter goroutine to the chain that drops multiples of p. The first value that reaches the end of the chain is always the next prime.
func generate(ctx context.Context) <-chan int {
ch := make(chan int)
go func() {
defer close(ch)
for i := 2; ; i++ {
select {
case ch <- i:
case <-ctx.Done():
return
}
}
}()
return ch
}
func filter(ctx context.Context, in <-chan int, prime int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for v := range in { // ends when upstream closes
if v%prime != 0 {
select {
case out <- v:
case <-ctx.Done():
return
}
}
}
}()
return out
}
func Primes(n int) []int {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // tears the whole chain down
ch := generate(ctx)
primes := make([]int, 0, n)
for range n {
p := <-ch
primes = append(primes, p)
ch = filter(ctx, ch, p)
}
return primes
}
Goroutine count: one generator plus one filter per prime found, so n + 1 goroutines for n primes. The last filter is created but never read from.
Shutdown without leaks: the classic Go-talk version has no stop mechanism, so every goroutine stays blocked on a send forever, and that is a leak. With ctx, cancel() wakes every goroutine that is blocked on a send. A filter blocked on receive is released by the cascade: its upstream returns, the deferred close runs, range in ends, and the filter closes its own out. Verify this in tests with go.uber.org/goleak or runtime.NumGoroutine().
Interviewer bonus: this is not the real Sieve of Eratosthenes. Each number is trial-divided by every smaller prime, so the work is roughly O(n² / log n), and every number costs one channel hop per stage. It is elegant for showing off CSP pipelines but slow, and a plain slice-based sieve is orders of magnitude faster.
More on Classic Concurrency Coding Problems
- Q522Implement a reusable barrier (CyclicBarrier) that N goroutines wait on before moving to the next phase.
- Q523Implement a Future/Promise type in Go with Get(ctx) and error propagation using generics.
- Q525Concurrently crawl URLs (the Tour of Go web crawler) with depth limits, deduplication and bounded parallelism.
- Q526Count word frequencies in a large set of files concurrently and return the top K words. How do you merge per-worker maps efficiently?
- Q527Implement a debouncer and a throttler for a stream of events using time.Timer and channels.
- Q528Implement a sharded concurrent map (N shards with separate mutexes). When does it beat sync.Map and a single RWMutex?