You already know how channels work in Golang. Before this, you were processing information for just a single channel. Today, you will learn how to handle multiple channels in a single block.
Basic usage
Let's consider a simple example with read-write operations for two channels:
package main
import "fmt"
func main() {
chan1 := make(chan int)
chan2 := make(chan int)
var data int
go func() {
for {
fmt.Scan(&data)
// write
select {
case chan1 <- data:
case chan2 <- data:
}
}
}()
for range [3]struct{}{} {
// read
select {
case data := <-chan1:
fmt.Println("Data from chan1:", data)
case data := <-chan2:
fmt.Println("Data from chan2:", data)
}
}
}
// Input:
// 200 403 505
// Output:
// Data from chan1: 200
// Data from chan2: 403
// Data from chan2: 505Be sure to try running the example above yourself. What did you see that was unusual? The most important thing is the output order, which may differ from what is shown in the example.
Let's figure out what happened. First, let's look at the goroutine that reads the data. It contains an infinite loop, which waits for input data on each iteration, followed by a select statement that writes the data entered into one of the channels. After this goroutine, there is a loop with three iterations in which we read from one of the channels. In both cases, the phrase "one of" is not accidental. Here are some basic rules for using the select operator when reading/writing channels:
If multiple channels are ready for read/write operations, the
selectstatement will pseudo-randomly choose one of them to handle the operation.If only one channel is ready for read/write operations, that channel will be selected.
If none of the channels are ready for read/write operations, the
selectstatement will either:Cause the current executing goroutine to block, waiting until at least one of the channels becomes ready for communication, or
Execute the
defaultbranch if it is defined, allowing for a non-blocking operation.
Default branch
Let's consider the following task. Suppose we have two employees, Jack and Rose, who work in the same department. Upon receiving a new task in the department, you need to assign an available employee to handle it. Here is a possible solution to this task:
package main
import "fmt"
func main() {
JackTasks := make(chan int, 1)
RoseTasks := make(chan int, 1)
var taskID int
for {
fmt.Scan(&taskID)
select {
case JackTasks <- taskID:
fmt.Printf("The task (%d) is assigned to Jack\n", taskID)
case RoseTasks <- taskID:
fmt.Printf("The task (%d) is assigned to Rose\n", taskID)
default:
fmt.Println("There are no available employees")
return
}
}
}
// Input: 32
// Output: The task (32) is assigned to Jack
// Input: 64
// Output: The task (64) is assigned to Rose
// Input: 128
// Output: There are no available employeesIn this example, there are no channel read operations. On the third iteration of the loop, both channels will be unavailable for writing. The default branch helps handle this case by preventing an error from occurring.
Timeout
In the following example, we will explore another use of the select statement when working with channels. As in the previous example, we receive the task number and assign an employee to complete it. But now, we want to track the progress of the task.
package main
import (
"fmt"
"time"
)
func main() {
JackTasks := make(chan int, 1)
var taskID int
fmt.Scan(&taskID)
JackTasks <- taskID // assign the task to Jack
timer := time.NewTimer(time.Second * 3) // set time to complete the task
defer timer.Stop()
for {
select {
case <-timer.C:
fmt.Printf("Jack has finished task (%d)", <-JackTasks)
return
default:
fmt.Println("Jack is working")
time.Sleep(time.Second)
}
}
}In the above example, the employee Jack has 3 seconds to complete the task and track its progress. time.Timer provides access to a channel that receives data when the specified time has elapsed. As long as there is no data in the time.Timer channel, the default branch of the select statement will be executed (the time.Sleep is used to reduce the output information). Once the time.Timer channel becomes ready for reading, the corresponding select branch is executed, and the program exits.
It is important to understand that the select statement does not block during the timer execution. The code inside the select statement runs immediately once it is started.
Block forever
Blocking the main goroutine is another, less obvious way to use the select statement. Suppose you have launched a program that serves a couple of goroutines. However, if you don't block the main goroutine, the program will exit:
package main
import (
"fmt"
"time"
)
func main() {
go func() {
for {
fmt.Println("The task is executed every 2 seconds")
time.Sleep(2 * time.Second)
}
}()
go func() {
for {
fmt.Println("The task is executed every 3 seconds")
time.Sleep(3 * time.Second)
}
}()
}The code from the above example will not output anything, as it terminates immediately. To prevent this, the main function needs to be blocked. You may already know that an infinite loop for {} blocks a goroutine. However, this method can lead to performance issues. Instead of blocking with an infinite loop, you can use a select statement for blocking, like this:
package main
import (
"fmt"
"time"
)
func main() {
go func() {
for {
fmt.Println("The task is executed every 2 seconds")
time.Sleep(2 * time.Second)
}
}()
go func() {
for {
fmt.Println("The task is executed every 3 seconds")
time.Sleep(3 * time.Second)
}
}()
select {}
}
// Output:
// The task is executed every 3 seconds
// The task is executed every 2 seconds
// The task is executed every 2 seconds
// The task is executed every 3 seconds
// The task is executed every 2 seconds
// ...Conclusion
Today, you learned about the select statement for working with channels. Let's summarize some key points:
The
selectstatement can be used for reading/writing operations on channels.The
selectstatement selects the operation that can be executed (if there are several such operations, one is selected randomly).The operations defined in the
selectstatement are executed immediately (remember this if you need to combine theselectstatement with time-based operations).The
selectstatement can be used to block a goroutine (select {}).