Go's channels are the synchronization primitive goroutines use to pass values
to each other, with a maxsize that changes their behavior fundamentally:
0 means unbuffered/synchronous, >0 means buffered. Since this exercise has
no real concurrency, "blocking" is modeled explicitly: an operation that
couldn't complete immediately returns a status saying it WOULD have blocked,
instead of actually blocking.
Buffered (maxsize > 0): send() succeeds immediately if the buffer
isn't full yet.
Unbuffered (maxsize == 0): an unbuffered channel has NO storage.
send() only succeeds if a receiver is ALREADY WAITING for a value, so the
two operations "rendezvous" directly. recv() on an empty unbuffered channel
doesn't just fail -- it registers itself as waiting (receivers_waiting), so
a SUBSEQUENT send() can find it and complete.
Closed (the part most people get wrong): implement close(), and make
send/recv respect it:
send()on a closed channel is a hard error -- raiseChannelClosedError.recv()on a closed channel still returns whatever's left in the buffer first; only once it's genuinely drained does it report(False, None).- Critically:
recv()on a closed, drained, UNBUFFERED channel must NOT register itself as a waiting receiver -- nothing will ever be sent on a closed channel again, so waiting for it would wait forever.
The missing link: closed isn't just "permanently empty"
It's tempting to treat a closed channel as "acts empty forever," but that
breaks two real things at once. First, a closed channel can still have
buffered values worth delivering -- closing doesn't discard them, it just
forbids any NEW sends. Second, and more subtly: the unbuffered recv() path
normally registers as waiting when there's nothing to receive, on the
assumption that a matching send() might still show up. Once the channel is
closed, that assumption is provably false -- registering as waiting anyway
would be a receiver waiting for something that can now never arrive, exactly
the kind of bug that turns into a real, silent goroutine leak in Go.
Example
ch = Channel(maxsize=0) # unbuffered
ch.send(1) # -> False (no receiver waiting -- would block)
ch.recv() # -> (False, None) (nothing to receive -- now waiting)
ch.send(42) # -> True (a receiver IS waiting now -- delivers)
ch.recv() # -> (True, 42)
ch.close()
ch.send(99) # raises ChannelClosedError
ch.recv() # -> (False, None) -- and does NOT register as waiting
Constraints
- A buffered channel's
send/recvnever touchesreceivers_waitingat all -- that bookkeeping only matters for the unbuffered case. - Once a rendezvous delivers a value,
receivers_waitingmust go back down -- a secondsend()right after should NOT succeed unless anotherrecv()registered as waiting again. recv()must check the buffer BEFORE checking whether the channel is closed -- closing doesn't discard what's already buffered.
Real-world grounding
- Go's channel implementation, including
closechan's handling of pending receivers:src/runtime/chan.go
Real Go channels use actual goroutine blocking/waking (parking a goroutine on
a wait queue, waking it when a match arrives, or when the channel closes)
instead of a boolean return value -- and closing a real Go channel actually
wakes every goroutine currently parked waiting to receive from it, handing
each one the zero value. This exercise's "don't register as waiting on an
already-closed channel" rule is the same guarantee, just checked at
recv()-time instead of via an explicit wake-up.