I'm reading the Concurrency in Go book from O'Reilly and found this code example:
doWork := func(
done <-chan interface{},
pulseInterval time.Duration,
) (<-chan interface{}, <-chan time.Time) {
heartbeat := make(chan interface{})
results := make(chan time.Time)
go func() {
defer close(heartbeat)
defer close(results)
pulse := time.Tick(pulseInterval)
workGen := time.Tick(2 * pulseInterval) // this just creates workload
sendPulse := func() {
select {
case heartbeat <- struct{}{}:
default:
}
}
sendResult := func(r time.Time) {
for {
select {
case <-done:
return
case <-pulse:
sendPulse()
case results <- r:
return
}
}
}
for {
select {
case <-done:
return
case <-pulse:
sendPulse()
case r := <-workGen:
sendResult(r)
}
}
}()
return heartbeat, results
}
It seemed very strange that two select statements were being used for a simple heartbeat. Then I understood that the reason behind this is that sendResults SHOULDN'T block trying to send a result to the results channel. If nobody is receiving from that channel, it will effectively block the goroutine and stop sending the heartbeat.
Then I thought... Why didn't they code it like this then?
doWork := func(
done <-chan interface{},
pulseInterval time.Duration,
) (<-chan interface{}, <-chan time.Time) {
heartbeat := make(chan interface{})
results := make(chan time.Time)
go func() {
defer close(heartbeat)
defer close(results)
pulse := time.Tick(pulseInterval)
workGen := time.Tick(2 * pulseInterval) // this just creates workload
sendPulse := func() {
select {
case heartbeat <- struct{}{}:
default:
}
}
for {
select {
case <-done:
return
case <-pulse:
sendPulse()
case results <- <- workgen:
fmt.Println("Send result")
}
}
}()
return heartbeat, results
}
And realized that it is because if workgen has an available item and I read it but then nobody is reading from results, the workgen item will be lost.
Question
Is there any other pattern to avoid these nested selects that have to repeat all the cases not to miss a value?
Thoughts
Wouldn't all these problems be solved IF a select statement first validated that the results channel is waiting to receive a value instead of executing the right hand expression first?
In other words, if I have this results <- <- workgen, it would be better if the select said "hey, nobody is reading from results, let's not try to evaluate <- workgen". This seems like a very particular design choice for me and I think it is what makes a lot of nested selects necessary to begin with.
I think a case should first try to evaluate if the channels that are in the expression are ready to receive or send data, and then execute the expressions.