select keywords
We can use the select keyword to listen to multiple goroutines at the same time.
package main import ( "fmt" "time" ) func main() { c1 := make(chan string) c2 := make(chan string) go func() { (1 * ) c1 <- ().String() }() go func() { (2 * ) c2 <- ().String() }() for i := 0; i < 2; i++ { select { case res1 := <-c1: ("from c1:", res1) case res2 := <-c2: ("from c2:", res2) } } }
from c1: 2022-09-04 14:30:39.4469184 -0400 EDT m=+1.000172801
from c2: 2022-09-04 14:30:40.4472699 -0400 EDT m=+2.000524401
The above code shows how the select keyword works:
- We first create two channels c1 and c2 to listen.
- Then we generate two goroutines, sending the current time to c1 and c2 respectively.
- In the for loop, we create a select statement and define two cases: the first case is when we can receive it from c1 and the second case is when we can receive it from c2.
You can see that the select statement and the switch statement are very similar in design. Both define different situations and run the corresponding code when a certain situation is satisfied. In addition, we can see that the select statement is blocking. That is, it will wait until one of the cases is satisfied.
We iterated twice for this loop because there are only two goroutines that need to be listened to. To be more precise, each goroutine is onefire-and-forget goroutine
, meaning they are sent to one channel only once before returning. So, in this code, there is a maximum value of two messages at any time, and we only need to choose twice.
If we don't know when the work will end
Sometimes we don't know how many jobs there are. In this case, place the select statement in a while loop.
package main import ( "fmt" "math/rand" "time" ) func main() { c1 := make(chan string) (().UnixNano()) for i := 0; i < (10); i++ { go func() { (1 * ) c1 <- ().String() }() } for { select { case res1 := <-c1: ("from c1:", res1) } } }
Because we have a random number goroutines run, we don't know how many jobs there are. Thankfully, the for loop wrapped in the bottom with the select statement will capture every output. Let's see what happens if we run this code.
from c1: 2022-09-04 14:48:47.5145341 -0400 EDT m=+1.000257801
from c1: 2022-09-04 14:48:47.5146126 -0400 EDT m=+1.000336201
from c1: 2022-09-04 14:48:47.5146364 -0400 EDT m=+1.000359901
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
()
/home/jacob/blog/testing/listening-to-multiple-channels-in-go/:22 +0x128
exit status 2
Well, the select statement was received three times as expected, but the program went wrong due to deadlock. Why does this happen?
Remember, the for loop in Go will run forever without any conditions. This means that the select statement will always try to receive. However, the number of jobs to be run is limited. Even without more work, the select statement still tries to receive.
Remember in the first article in this series I said that if you try to receive from a channel without buffering when the sender is not ready, your program will be deadlocked. This is exactly what we have in our example.
So how do we solve this problem? We can use a combination of concepts covered in previous articles: Exit Channel and WaitGroups.
package main import ( "fmt" "math/rand" "sync" "time" ) func main() { c1 := make(chan string) exit := make(chan struct{}) (().UnixNano()) var wg go func() { numJob := (10) ("number of jobs:", numJob) for i := 0; i < numJob; i++ { (1) go func() { defer () (1 * ) c1 <- ().String() }() } () close(exit) }() for { select { case res1 := <-c1: ("from c1:", res1) case <-exit: return } } }
3
from c1: 2022-09-04 15:09:08.6936976 -0400 EDT m=+1.000287801
from c1: 2022-09-04 15:09:08.6937788 -0400 EDT m=+1.000369101
from c1: 2022-09-04 15:09:08.6937949 -0400 EDT m=+1.000385101
- Let's create a
exit channel
and a WaitGroup. - The number of jobs run is random. For the number of times numJobs, we start goroutines. To wait for the jobs to be completed, we add them to wg. When a job is finished, we subtract one from wg.
- Once all work is done, we close
exit channel
。 - We wrap the above sections in a goroutine because we want all sections to be unblocked. If we don't wrap it in a goroutine, () will wait until the job is completed. This will block the code and will not let the bottom for-select statement run.
- Furthermore, since c1 is an unbuffered channel, waiting for all goroutines to send messages to c1 will result in many messages being sent to c1 without the for-select statement being received. This leads to a deadlock because when the sender is ready, the receiver is not ready.
How to make select non-blocking
The select statement is blocking by default. How do we make it non-blocking? It's simple - we just need to add a default case.
package main import ( "fmt" "math/rand" "sync" "time" ) func main() { ashleyMsg := make(chan string) brianMsg := make(chan string) exit := make(chan struct{}) (().UnixNano()) var wg go func() { numJob := (10) ("number of jobs:", numJob) for i := 0; i < numJob; i++ { (2) go func() { defer () (((10)) * ) ashleyMsg <- "hi" }() go func() { defer () (((10)) * ) brianMsg <- "what's up" }() } () close(exit) }() for { select { case res1 := <-ashleyMsg: ("ashley:", res1) case res2 := <-brianMsg: ("brian:", res2) case <-exit: ("chat ended") return default: ("...") () } } }
...
number of jobs: 4
brian: what's up
...
ashley: hi
...
...
brian: what's up
ashley: hi
ashley: hi
brian: what's up
...
...
ashley: hi
...
brian: what's up
...
chat ended
Apart from the crappy conversations, we can see how it works by default. Rather than waiting for the chat history to arrive, we can do something when there is no channel to receive. In this example, we just print out the ellipsis, but you can do whatever you want.
This is the end of this article about how to listen to multiple channels in Golang. For more related content on listening to multiple channels, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!