Select Statement in Go
In this tutorial, we are going to discuss the select statement in the Go language.
In Go language, the select statement is just like a switch statement, but in the select statement, the case statement refers to communication, i.e. sent or receive operation on the channel.
select {
case SendOrReceive1: // Statement
case SendOrReceive2: // Statement
case SendOrReceive3: // Statement
.......
default: // Statement
}
The select statement is used to choose from multiple send/receive channel operations. The select statement blocks until one of the send/receive operations are ready.
If multiple operations are ready, one of them is chosen at random. The syntax is similar to switch except that each of the case statements will be a channel operation.
Lets stop the theory and write some code right away ?
package main
import (
"fmt"
"time"
)
func server1(ch chan string) {
time.Sleep(6 * time.Second)
ch <- "from server1"
}
func server2(ch chan string) {
time.Sleep(3 * time.Second)
ch <- "from server2"
}
func main() {
output1 := make(chan string)
output2 := make(chan string)
go server1(output1)
go server2(output2)
select {
case s1 := <-output1:
fmt.Println(s1)
case s2 := <-output2:
fmt.Println(s2)
}
}
Output
from server2
In the above example, the server1 function sleeps for 6 seconds then writes the text from server1 to the channel ch. The server2 function sleeps for 3 seconds and then writes from server2 to the channel ch.
Now the main function calls the go Goroutines server1 and server2.
Next, the control reaches the select statement. The select statement blocks until one of its cases are ready.
In our example, server1 Goroutine writes to the output1 channel after 6 seconds, whereas server2 writes to the output2 channel after 3 seconds.
So the select statement will block for 3 seconds and wait for server2 Goroutine to write to the output2 channel. After 3 seconds, the program prints from server2 and then will terminate.
Practical use of select
The reason behind naming the functions in the above program as server1 and server2 is to illustrate the practical use of select.
Let’s assume we have a mission-critical application, and we need to return the output to the user as quickly as possible. The database for this application is replicated and stored in different servers across the world.
Assume that the functions server1 and server2 are, in fact communicating with 2 such servers. The response time of each server is dependent on the load on each and the network delay.
Now we send the request to both the servers and then wait on the corresponding channels to respond using the select statement.
The server which responds first is chosen by the select, and the other response is ignored. This way, we can send the same request to multiple servers and return the quickest response to the user.
Default case
The default case in a select statement is executed when none of the other case is ready. This is generally used to prevent the select statement from blocking.
package main
import (
"fmt"
"time"
)
func process(ch chan string) {
time.Sleep(5500 * time.Millisecond)
ch <- "process successful"
}
func main() {
ch := make(chan string)
go process(ch)
for {
time.Sleep(1000 * time.Millisecond)
select {
case v := <-ch:
fmt.Println("Received value is: ", v)
return
default:
fmt.Println("No value received")
}
}
}
Output
No value received
No value received
No value received
No value received
No value received
Received value is: process successful
In the program above, the process function sleeps for 5500 milliseconds (5.5 seconds) and then writes the process successfully to the ch channel. This function is called concurrently.
After calling the process Goroutine concurrently, an infinite for loop is started in the main Goroutine.
The infinite loop sleeps for 1000 milliseconds (1 second) during the start of each iteration and then performs a select operation.
During the first 5500 milliseconds, the first case of the select statement, namely case v := <-ch: will not be ready since the process Goroutine will write to the ch channel only after 5500 milliseconds.
Hence the default case will be executed during this time, and the program will print no value received 5 times.
After 5.5 seconds, the process Goroutine writes process successful to ch. Now the first case of the select statement will be executed, and the program will print Received value is: process successful and then it will terminate.
Deadlock and default case
package main
func main() {
ch := make(chan string)
select {
case <-ch:
}
}
In the program above, we have created a channel ch. We try to read from this channel inside the select statement. The select statement will block forever since no other Goroutine is writing to this channel, resulting in deadlock.
So this program will panic at runtime with the following message,
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
/tmp/sandbox416567824/main.go:6 +0x80
If a default case is present, this deadlock will not happen since the default case will be executed when no other case is ready. The program above is rewritten with a default case below.
package main
import "fmt"
func main() {
ch := make(chan string)
select {
case <-ch:
default:
fmt.Println("Default case executed")
}
}
Output
Default case executed
Similarly, the default case will be executed even if the select has only nil channels.
package main
import "fmt"
func main() {
var ch chan string
select {
case v := <-ch:
fmt.Println("Received value", v)
default:
fmt.Println("Default case executed")
}
}
Output
Default case executed
In the above program ch is nil, and we are trying to read from ch in the select statement. If the default case was not present, the select would have been blocked forever and caused a deadlock.
Random selection
When multiple cases in a select statement are ready, one of them will be executed at random.
package main
import (
"fmt"
"time"
)
func server1(ch chan string) {
ch <- "from server1"
}
func server2(ch chan string) {
ch <- "from server2"
}
func main() {
output1 := make(chan string)
output2 := make(chan string)
go server1(output1)
go server2(output2)
time.Sleep(1 * time.Second)
select {
case s1 := <-output1:
fmt.Println(s1)
case s2 := <-output2:
fmt.Println(s2)
}
}
In the above program, server1 and server2 goroutines are called in the main Goroutine. Then the main program sleeps for 1 second.
When the control reaches the select statement, server1 would have written from server1 to the output1 channel, and server2 would have written from server2 to the output2 channel, and hence both the cases of the select statement are ready to be executed.
If you run this program multiple times, the output will vary between from server1 or from server2 depending on which case is chosen randomly.
Please run this program in your local system to get this randomness. If this program is run in the playground, it will print the same output since the playground is deterministic.
Gotcha – Empty select
package main
func main() {
select {}
}
What do you think will be the output of the program above?
We know that the select statement will block until one of its cases is executed. In this case, the select statement doesn’t have any cases, and hence it will block forever, resulting in a deadlock.
This program will panic with the following output,
atal error: all goroutines are asleep - deadlock!
goroutine 1 [select (no cases)]:
main.main()
/tmp/sandbox299546399/main.go:4 +0x20
That’s all about the Select Statement in Go language. If you have any queries or feedback, please write us email at contact@waytoeasylearn.com. Enjoy learning, Enjoy Go language.!!