Under The Hood
backend go Go 1.21+

Go channels: the hchan struct and lock-based design

Last updated
Prerequisites:
Go goroutines and channels basics
Understanding of mutexes and condition variables
  • go
  • channels
  • hchan
  • concurrency
  • synchronization
  • runtime

Read at your depth

The practical view

ch := make(chan int, 3) creates a buffered channel. Sends and receives block when the buffer is full/empty respectively; unbuffered channels (make(chan int)) block until the other side is ready, making them synchronization points. close(ch) signals 'no more sends'; ranging (for v := range ch) drains until close. The primary use cases: hand-off synchronization between goroutines, work queues (buffered channels + workers), and graceful shutdown signals (close(ch) as a broadcast). select multiplexes over multiple channels: select { case v := <-ch1: ...; case ch2 <- v: ...; default: ... }.

Legacy vs modern

Unbuffered handoff vs buffered work queue

Unbuffered channels synchronize two goroutines with a rendezvous; buffered channels decouple producers from workers and need explicit close for completion.

before → after
Unbuffered broadcast (blocking main)
done := make(chan struct{}) // ❌ OLD: unbuffered → rendezvous, main blocks on send
go func() { work(); done <- struct{}{} }()
<-done // blocks until the goroutine sends
Buffered queue + close broadcast
jobs := make(chan int, 10) // ✅ NEW: buffered(10) + close(jobs) broadcasts completion
// N workers; close(jobs) broadcasts completion, workers drain

Interview gotchas

Context

This distinguishes candidates who assume channels are high-performance lock-free queues from those who know the runtime's actual design and its implications.

The mechanical answer

Every hchan operation takes the channel's mutex (hchan.lock) — sends, receives, and close all serialize on it. So a hot buffered channel is contended the way a mutex-protected queue is: throughput is bounded by lock hold time and cache-line bouncing, not by lock-free atomics. The runtime chose this for correctness and simplicity: FIFO order is guaranteed by the wait queues, memory ordering is handled by the lock, and the ring buffer keeps buffer management trivial. The practical implication: for very high-throughput work distribution, a hand-rolled sharded queue or Go 1.22+ iterators/slice-based pools can beat a single channel; channels should be used for clarity and correctness, with the lock cost accepted.

Trap

The trap answer is 'channels are implemented with atomics, so they are super fast'. The hchan uses a full mutex; only select's randomization and some fast paths avoid locking. Another trap: 'buffered channels are async, so they never block' — they block when the buffer is full (sends) or empty (receives). Candidates who mention hchan's lock field by name and the sudog wait queues demonstrate real runtime knowledge.

Under The Hood — a multi-depth technical interview hub.

Third-party licenses Press ⌘ K to search.