How would you implement a semaphore / bounded concurrency with channels?
Question 203MediumGo 1.22 to 1.25
A buffered channel of capacity N acts as a counting semaphore: send to acquire, receive to release.
func processAll(ctx context.Context, items []Item, limit int) error {
sem := make(chan struct{}, limit)
var wg sync.WaitGroup
for _, it := range items { // Go 1.22: 'it' is per-iteration
select {
case sem <- struct{}{}:
case <-ctx.Done():
wg.Wait()
return ctx.Err()
}
wg.Add(1)
go func() {
defer wg.Done()
defer func() { <-sem }()
process(it)
}()
}
wg.Wait()
return nil
}
Key points: acquire before spawning the goroutine, so that at most N goroutines exist, not just N running. struct{} takes zero bytes. In production code, golang.org/x/sync/errgroup with g.SetLimit(n) or x/sync/semaphore (weighted) are usually better choices, and interviewers like hearing that you know them. The alternative is a fixed worker pool reading from a jobs channel, which works better when setting up each worker is expensive.
More on Channels & select
- Q201How do you do a non-blocking send or receive?
- Q202What does this print? (select evaluation order)
- Q204Implement a worker pool with result collection and proper shutdown.
- Q205What is a goroutine leak caused by channels? Show one and fix it.
- Q206Why is
chan struct{}preferred for signaling, and how doescloseact as a broadcast? - Q207What does this print? (len and cap of channels)