Here's a general idiom that solves the select's priority problem.
Yes, it's not nice to say a least, but does what is needed for 100%, no pitfalls and no hidden limitations.
Here's a short code example, and explanation follows.
package main
import(
"fmt"
"time"
)
func sender(out chan int, exit chan bool) {
for i := 1; i <= 10; i++ {
out <- i
}
time.Sleep(2000 * time.Millisecond)
out <- 11
exit <- true
}
func main(){
out := make(chan int, 20)
exit := make(chan bool)
go sender(out, exit)
time.Sleep(500 * time.Millisecond)
L:
for {
select {
case i := <-out:
fmt.Printf("Value: %d\n", i)
default:
select {
case i := <-out:
fmt.Printf("Value: %d\n", i)
case <-exit:
select {
case i := <-out:
fmt.Printf("Value: %d\n", i)
default:
fmt.Println("Exiting")
break L
}
}
}
}
fmt.Println("Did we get all 10? Yes.")
fmt.Println("Did we get 11? DEFINITELY YES")
}
And, here's how it works, the main()
from above, annotated:
func main(){
out := make(chan int, 20)
exit := make(chan bool)
go sender(out, exit)
time.Sleep(500 * time.Millisecond)
L:
for {
select {
// here we go when entering next loop iteration
// and check if the out has something to be read from
// this select is used to handle buffered data in a loop
case i := <-out:
fmt.Printf("Value: %d\n", i)
default:
// else we fallback in here
select {
// this select is used to block when there's no data in either chan
case i := <-out:
// if out has something to read, we unblock, and then go the loop round again
fmt.Printf("Value: %d\n", i)
case <-exit:
select {
// this select is used to explicitly propritize one chan over the another,
// in case we woke up (unblocked up) on the low-priority case
// NOTE:
// this will prioritize high-pri one even if it came _second_, in quick
// succession to the first one
case i := <-out:
fmt.Printf("Value: %d\n", i)
default:
fmt.Println("Exiting")
break L
}
}
}
}
fmt.Println("Did we get all 10? Yes.")
fmt.Println("Did we get 11? DEFINITELY YES")
}
NOTE: Before playing tricks with prioritizations, MAKE SURE YOU ARE SOLVING THE RIGHT PROBLEM.
Chances are, it can be solved differently.
Still, to have prioritized select in Go would have been great thing. Just a dream..
NOTE: This is quite a similar answer https://stackoverflow.com/a/45854345/11729048 on this thread, but there is only two select
-s are nested, not three ones as I did. What's the difference? My approach is more efficient, and there we explicitly expect to handle random choices on each loop iteration.
However, if the high-priority channel isn't buffered, and/or you don't expect bulk data on it, only the sporadic single events,
then the simpler two-stage idiom (as in that answer) will suffice:
L:
for {
select {
case i := <-out:
fmt.Printf("Value: %d\n", i)
case <-exit:
select {
case i := <-out:
fmt.Printf("Value: %d\n", i)
default:
fmt.Println("Exiting")
break L
}
}
}
It is basically 2 and 3 stages, the 1 being removed.
And once again: in like 90% cases you think you do need to prioritize chan switch cases, you really don't.
And here's a one-liner, that can be wrapped in a macro:
for {
select { case a1 := <-ch_p1: p1_action(a1); default: select { case a1 := <-ch_p1: p1_action(a1); case a2 := <-ch_p2: select { case a1 := <-ch_p1: p1_action(a1); default: p2_action(a2); }}}
}
And what if you want to prioritize more than two cases?
Then you have two options. First one - build a tree, using intermediate goroutines, so that each fork is exactly binary (the above idiom).
The second option is to make the priority-fork more then double.
Here's an example of three priorities:
for {
select {
case a1 := <-ch_p1:
p1_action(a1)
default:
select {
case a2 := <-ch_p2:
p2_action(a2)
default:
select { // block here, on this select
case a1 := <-ch_p1:
p1_action(a1)
case a2 := <-ch_p2:
select {
case a1 := <-ch_p1:
p1_action(a1)
default:
p2_action(a2)
}
case a3 := <-ch_p3:
select {
case a1 := <-ch_p1:
p1_action(a1)
case a2 := <-ch_p2:
p1_action(a2)
default:
p2_action(a3)
}
}
}
}
}
That is, the whole structure is conceptually split into three parts, as the original (binary) one.
One again: chances are, you can design your system so that you can avoid this mess.
P.S., the rhetoric question: why Golang doesn't have it built in into the language??? The question is rhetoric one.