replacing condition variables with channel ops, better DST support.

280 views
Skip to first unread message

Jason E. Aten

unread,
Oct 30, 2025, 3:48:38 AMOct 30
to golang-nuts
If you ever wished for 

+ better Deterministic Simulation Testing (DST) support, or 

+ that channels could replace Condition Variables, or for

+ a sync.WaitGroup alternative that could be interrupted by a timeout, or that

+ channel close could be idempotent and broadcast a non-zero value,

then I invite you to experiment with my superset-of-Go experiment, Pont.


Feedback welcome -- post an issue on the repo.

Jason

Will Faught

unread,
Nov 2, 2025, 5:42:50 PM (10 days ago) Nov 2
to Jason E. Aten, golang-nuts
Looks interesting! Is there an article somewhere that explains why each change to Go was necessary for DST? For example, why are sticky channel values required for DST? Couldn’t sticky values be simulated with a goroutine that owns sending on a channel that implements the sticky/unsticky/clear behavior? Isn’t it possible in general to arrange to not close a channel more than once?

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/golang-nuts/c7dc4d42-0f31-4960-9497-a7855827b444n%40googlegroups.com.

Jason E. Aten

unread,
Nov 2, 2025, 8:34:40 PM (10 days ago) Nov 2
to golang-nuts
On Sunday, November 2, 2025 at 10:42:50 PM UTC Will Faught wrote:
Looks interesting! Is there an article somewhere that explains why each change to Go was necessary for DST? For example, why are sticky channel values required for DST?

Hi Will,

Thanks! They aren't. Both are just things I wished for. :) Thanks for asking. I may add a note to clarify that.
 
Couldn’t sticky values be simulated with a goroutine that owns sending on a channel that implements the sticky/unsticky/clear behavior?

I'm not sure what you are thinking of with a goroutine somehow "owning" a channel...?

I certainly have been thinking about how to do ownership, since it is 
clear that Go could benefit from Pony/Rust like ownership
checking to prevent data races, and having an "owner" type that the runtime could 
change and check would be an obvious approach. But I'm not sure how... let
me know if you have ideas.

I did try to do "closing values" (in https://github.com/glycerine/loquet) without 
hacking on the runtime, but I got to many races. You
really do want the broadcast value to come "over the channel", not alongside it.
 
Isn’t it possible in general to arrange to not close a channel more than once?

Of course -- I wrote https://github.com/glycerine/idem to do that in 2017. 

My feeling was it should not need to be that heavy weight.  

Any time you see a huge number of workarounds (all
of the sync package facilities, cannot substitute for condition variables, integrates 
poorly with wait groups and condition variables), it should be a sign that your design 
primitives are not strong or orthogonal enough-- you are only rally covering a sub-space
(in the linear algebra sense) of the actual need-space.

Cheers,
Jason

Ja kub

unread,
Nov 2, 2025, 9:35:44 PM (10 days ago) Nov 2
to golang-nuts
"A: Yes, except for one thing. That one thing is a behavior you are almost surely not depending on, but may have cursed excessively at in the past: closing a channel twice in Go causes a panic. Closing a channel in Pont never panics, no matter how many times you close it."

I love it and never understood why in Go closing a channel more than once causes a panic. Can someone explain why is that?

Ian Lance Taylor

unread,
Nov 2, 2025, 9:39:56 PM (10 days ago) Nov 2
to Ja kub, golang-nuts
On Sun, Nov 2, 2025 at 6:35 PM Ja kub <pece...@gmail.com> wrote:
>
> "A: Yes, except for one thing. That one thing is a behavior you are almost surely not depending on, but may have cursed excessively at in the past: closing a channel twice in Go causes a panic. Closing a channel in Pont never panics, no matter how many times you close it."
>
> I love it and never understood why in Go closing a channel more than once causes a panic. Can someone explain why is that?

Closing a channel is an operation done on the sender side of a channel
to signal that no further information will be sent on the channel. If
there are multiple goroutines closing the channel, that means that
multiple goroutines need to somehow coordinate that there is nothing
else to send. If the goroutines do that coordination, it's a fairly
minor addition to coordinate which goroutine actually calls close. If
the goroutines don't do that goroutine, there is likely a program
error, and a panic calls attention to that.

Ian

Jason E. Aten

unread,
Nov 3, 2025, 9:06:31 PM (9 days ago) Nov 3
to golang-nuts
Thanks, Ja kub, for the enthusiasm. :)

Thanks, Ian, for the background.

Ja kub: If it helps, I never close channels
that are sent on. I recommend this approach.

Once one adopts this approach, then the rationale
for double close being a panic disappears.

The irony is not lost on me that Ian's, "somehow

coordinate that there is nothing
else to send" is the purpose for which I
invaraibly deploy idempotent close. I use
it for cancellation, erroring out, bailing out,
and sub-system or full-system shutdown.

(Close is the only broadcast primitive available
that plays well with select, and thus with timeouts.)

Pont also liberates close() from its 1-bit straight-jacket,
as Pont can broadcast pointers or any value. :)

Cheers,
Jason

Will Faught

unread,
Nov 4, 2025, 12:20:12 AM (9 days ago) Nov 4
to Jason E. Aten, golang-nuts
I had in mind a goroutine that receives commands for channel-like operations and implements them itself. So it would have a list to store the sent values, a sticky value variable, lists of receive commands (containing the channel to send a value back on), and lists of receive-full commands (containing the channel to send the values back on). It would loop on selecting for incoming commands, and behave accordingly. It’s not as efficient as having the behavior built in, no doubt, but probably simpler to implement.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.

Jason E. Aten

unread,
Nov 4, 2025, 11:18:50 AM (9 days ago) Nov 4
to golang-nuts
On Tuesday, November 4, 2025 at 5:20:12 AM UTC Will Faught wrote:
I had in mind a goroutine that receives commands for channel-like operations and implements them itself. So it would have a list to store the sent values, a sticky value variable, lists of receive commands (containing the channel to send a value back on), and lists of receive-full commands (containing the channel to send the values back on). It would loop on selecting for incoming commands, and behave accordingly. It’s not as efficient as having the behavior built in, no doubt, but probably simpler to implement.

Nice. I have been thinking along similar lines. 

The difficulty is one of implementation. It's such a large departure from the
existing code that I would not know how to even begin to make it work.

Current channel ops never do that behavior, which would 
involve a) starting and/or resuming a goroutine; b) blocking on it's finish/yielding; and c) then
resuming after a "3rd party" goroutine that is not otherwise involved in 
blocking on a channel.

It might be easier to associate one channel to "monitor" or proxy another channel, and to 
implicitly have the monitor channel interpose when the monitored channel
was considered for a channel op.  (I am thinking of how the P formal modeling language
uses monitors to check correctness; https://p-org.github.io/P/whatisP/ )

This is also more in line with my select discipline -- where in production I never allow just a single channel in a 
select and never use range over channel. To be able to cancel any
operation, there always must be at least two channels used together. 
Experimenting with explicit support for that convention might be 
an interesting research direction.

Hannes Stauss

unread,
Nov 4, 2025, 3:32:11 PM (9 days ago) Nov 4
to golang-nuts
This is an interesting approach. 

I would have following question/feedback to the design:
  1. In view of the intent to broadcast a non empty value to all receivers of a channel, my intuition would be that the receivers should receive the value only once. What was the rational behind the continuing receiving of the "sticky" value from a logical point of view? Maybe a broadcast send could be initiated with ch <* x. The receiving goroutine would receive the value and would park until the channel is signaled to be ready for another value, either a sticky or a non sticky one. This could eliminate the application logic part of versioning the receives and unnecessary loops on that.
  2. The block until full receive x <| ch seems to be a means to synchronize on goroutine termination in a select thus allowing to implement a timeout. However, one would need to know the number of goroutines to synchronize for beforehand - which seems not to be a universal case. The intent to have a timeout on Wait could simply be achieved by Wait() replicating the context Done() logic.
Hannes

Jason E. Aten

unread,
Nov 4, 2025, 5:05:19 PM (8 days ago) Nov 4
to golang-nuts
On Tuesday, November 4, 2025 at 8:32:11 PM UTC Hannes Stauss wrote:
This is an interesting approach. 

Thank you.
 
I would have following question/feedback to the design:
  1. In view of the intent to broadcast a non empty value to all receivers of a channel, my intuition would be that the receivers should receive the value only once. What was the rational behind the continuing receiving of the "sticky" value from a logical point of view? Maybe a broadcast send could be initiated with ch <* x. The receiving goroutine would receive the value and would park until the channel is signaled to be ready for another value, either a sticky or a non sticky one. This could eliminate the application logic part of versioning the receives and unnecessary loops on that.
For other readers, Hannes is referring to this discussion
of how we can replace condition variable uses cases
with sticky sends (https://github.com/glycerine/pont?tab=readme-ov-file#10-with-version-numbers-readers-can-poll-conditions).

That would be less general than condition variables,
and is readily achieved today by having the receiver nil out
their copy of the channel after they have
received on it however many times they wish.

Generally we don't want to store extra state in the 
channel if we can help it. Currently I've added only
two integers to each channel (one for sticky sends,
one for final sends), and even that feels kind of rich :)
  1. The block until full receive x <| ch seems to be a means to synchronize on goroutine termination in a select thus allowing to implement a timeout. However, one would need to know the number of goroutines to synchronize for beforehand - which seems not to be a universal case. The intent to have a timeout on Wait could simply be achieved by Wait() replicating the context Done() logic.
Correct. The full-receive operator defines "full" according
to the size of the channel's capacity, which is
a constant quantity once the channel
is made. If that isn't known, I'm not sure how
one would define "full".

Yes this indeed was aimed at the common case-- when
you can allocate a channel of fixed size corresponding
to the number of known subtask goroutines will
start shortly thereafter.

For more complex scenarios, I think you would need
to deploy supervision trees ala Erlang. I have written
a package for this. I typically use trees of idem.Halter
from my https://github.com/glycerine/idem
package, as it already solves the otherwise tricky
lock ordering deadlocks that one can run into when multiple
packages use supervision trees together. 

Thanks again for your interest. Any other questions, let me know.

Jason
Reply all
Reply to author
Forward
0 new messages