Go

Implement a bounded blocking queue (producer-consumer) using sync.Cond, then using channels. Compare the two.

Question 521HardGo 1.22 to 1.25

The Cond version uses a ring buffer and two condition variables that share one mutex. Put waits while the queue is full and Take waits while it is empty. Close broadcasts to both so that nobody blocks forever.

var ErrClosed = errors.New("queue closed")

type BlockingQueue[T any] struct {
	mu                *sync.Mutex
	notEmpty, notFull *sync.Cond
	buf               []T
	head, size        int
	closed            bool
}

func NewBlockingQueue[T any](capacity int) *BlockingQueue[T] {
	if capacity <= 0 {
		panic("capacity must be > 0")
	}
	mu := &sync.Mutex{}
	return &BlockingQueue[T]{mu: mu, notEmpty: sync.NewCond(mu),
		notFull: sync.NewCond(mu), buf: make([]T, capacity)}
}

func (q *BlockingQueue[T]) Put(v T) error {
	q.mu.Lock()
	defer q.mu.Unlock()
	for q.size == len(q.buf) && !q.closed {
		q.notFull.Wait()
	}
	if q.closed {
		return ErrClosed
	}
	q.buf[(q.head+q.size)%len(q.buf)] = v
	q.size++
	q.notEmpty.Signal()
	return nil
}

func (q *BlockingQueue[T]) Take() (T, bool) {
	q.mu.Lock()
	defer q.mu.Unlock()
	for q.size == 0 && !q.closed {
		q.notEmpty.Wait()
	}
	var zero T
	if q.size == 0 { // closed and drained
		return zero, false
	}
	v := q.buf[q.head]
	q.buf[q.head] = zero // drop reference for GC
	q.head = (q.head + 1) % len(q.buf)
	q.size--
	q.notFull.Signal()
	return v, true
}

func (q *BlockingQueue[T]) Close() {
	q.mu.Lock()
	q.closed = true
	q.mu.Unlock()
	q.notEmpty.Broadcast()
	q.notFull.Broadcast()
}

The channel version is simply q := make(chan T, capacity). q <- v is Put, v, ok := <-q is Take, and close(q) is Close, which only the producer side may call. Wrap the operations in a select with ctx.Done() to get cancellation.

Comparison: channels are shorter, correct by construction, and composable with select, so timeouts and cancellation come for free. Cond.Wait cannot be cancelled or given a timeout. With channels, however, sending on a closed channel panics, and you cannot peek, drain in batches, or re-size. Cond gives you those extras: Len, priority ordering, "wait until the queue drops below a watermark", and closing from either side. Performance is similar. Channels are themselves a mutex plus wait queues inside the runtime. What the interviewer is looking for: Wait in a for loop, two separate conditions so that Signal wakes the right kind of waiter, and a correct close/drain semantic.

More on Classic Concurrency Coding Problems

All 16 Classic Concurrency Coding Problems questions