Go's 10-Year Select Proposal Revived: bradfitz's Mutex.LockChan() Demo
The article traces the 10-year history of Go issue #16620, which sought to make sync.Cond selectable, explains the level-vs-edge-triggered conflict, reviews community workarounds, and details bradfitz's new Mutex.LockChan() demo that enables cancellable locking via select.
Ten Years Ago: A Real Pain Point from the HTTP/2 Battlefield
In 2016, while developing Go's HTTP/2 implementation, bradfitz encountered a concrete scenario: he needed a condition variable for flow control while simultaneously waiting on multiple channels, including net/http.Request.Cancel. He often had to wait for any of several events — flow-control condition satisfied, user cancelled the request, or peer closed the connection — to happen first.
The problem was that sync.Cond could not appear in a select statement. He proposed adding a WaitChan() method to Cond that would return a channel notified when the condition variable was signaled.
The Real Blocker: Two Fundamentally Different Trigger Models
Go core member ianlancetaylor provided the crucial insight: channels are level-triggered, condition variables are edge-triggered.
A channel's "has a value" state persists until the value is read.
A condition variable's Signal / Broadcast is a one-time event: it wakes goroutines waiting at that exact moment; if no one is waiting, the notification is lost and not stored.
Forcing an edge-triggered signal into a level-triggered channel model forces awkward choices: either manually drain the signal when no receiver exists, or produce far more wake-ups than expected. Broadcast is especially thorny — it means "wake all currently waiting goroutines." Translated to channel semantics, questions arise: should every goroutine selecting on that channel be woken? What value should be sent?
Because of this, ianlancetaylor concluded the issue was not a simple API patch but a "quite subtle design" change.
Community Workarounds Over Ten Years
Extra goroutine bridge: A dedicated goroutine blocks on cond.Wait() and notifies the real worker via a channel. Simple but leaks goroutines when the outer context cancels before the bridge goroutine can be cleaned up.
context.AfterFunc + Cond.Broadcast combo: Since Go 1.21, context.AfterFunc can call cond.Broadcast() on cancellation, waking waiters to check the context. Simpler than a bridge goroutine, but requires Broadcast (not Signal) because a cancellation-triggered broadcast might be "stolen" by another waiter if only Signal is used. (Example from benhoyt in the pebble project.)
Third-party libraries: Libraries like condchan and missinggo 's chancond attempted pure-channel cancellable condition variables. However, Go core member rogpeppe demonstrated races and panics under concurrent Signal / Broadcast calls, confirming ianlancetaylor's assessment.
Prerequisite unblocked: A long-standing dependency, issue #8898 (runtime support for new channel types/semantics), was resolved via #61542, removing the theoretical blocker. ianlancetaylor explicitly noted the proposal was "unblocked" again.
Turning Point: bradfitz Returns with a Demo CL
Ten years later, bradfitz submitted Demo CL 828544 (go-review.googlesource.com/c/go/+/828544). Notably, it does not implement the originally envisioned Cond.WaitChan(). Instead, it takes a narrower, more verifiable entry point: an experimental LockChan() method on sync.Mutex.
The design doc describes: calling mu.LockChan() returns a channel that is closed exactly when the lock is successfully acquired ; once a value is received, subsequent receives see the closed state without re-acquiring the lock; each LockChan() call returns a one-time-use channel.
Usage with context:
select {
case <-mu.LockChan():
// lock acquired in this branch, remember mu.Unlock()
case <-ctx.Done():
// request cancelled, lock not acquired
}For repeated locking with cancellation in a loop:
for {
select {
case <-mu.LockChan():
// do work
mu.Unlock()
case <-ctx.Done():
return ctx.Err()
}
}The demo sparked immediate discussion. One commenter asked a critical boundary detail: in a for { select { ... } } loop, does each mu.LockChan() call produce a fresh, independent channel? This directly affects safety in retry loops and is a key test case for the design.
What This Step Solves — and What Remains
Solved: LockChan() converts the lock-acquisition edge event into a one-time, unidirectional channel close . Closing is idempotent and safely receivable multiple times, neatly sidestepping the core level-vs-edge conflict for Signal scenarios.
This explains why the demo starts with Mutex rather than Cond: mutex acquisition is inherently exclusive and occurs once, naturally fitting a "close-once channel"; Cond.Broadcast must wake arbitrary numbers of waiters in varying states, a completely different complexity level.
Unresolved:
This is still only a demo (experimental code) , not yet in the formal proposal review process, let alone a Go release.
It covers only Mutex; the original issue's target — sync.Cond (especially Broadcast) — has no corresponding solution yet.
Whether RWMutex needs an RLockChan() and how to design it remains open.
Why This Matters
Even as a demo, Mutex.LockChan() signals a significant direction: Go may finally gain standard-library native support for "acquire lock while responding to cancellation/timeout," a common need in network services, connection pools, and rate limiters — without hand-written bridge goroutines or context.AfterFunc patches.
For long-time issue followers (e.g., the developer repeatedly asking detailed questions), the original author's return after a decade is a positive signal: the problem that has vexed the Go community for ten years is once again being seriously advanced.
References
Go issue #16620: github.com/golang/go/issues/16620
bradfitz's Demo CL 828544: go-review.googlesource.com/c/go/+/828544 context.AfterFunc documentation: pkg.go.dev/context#AfterFunc
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
TonyBai
Tony Bai's tech world (tonybai.com). Not satisfied with just "knowing how", we strive for mastery. Focused on Go language internals, high-quality engineering practices, and cloud‑native architecture, exploring cutting‑edge intersections of Go and AI. Gophers who pursue technology are welcome—follow me and evolve with Go.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
