Golang select Statement In Detail

Prerequisites

Knowledge of Goroutines and Channels

Introduction

The “select” statement in the Golang programming language is a powerful tool for handling multiple channel operations simultaneously.

It allows a Golang program to wait for multiple communication operations to complete and proceed when one of them is ready.

The syntax of the “select” statement resembles the switch statement, but the cases in a select statement are channel operations, not expressions.

Each case in a select statement specifies a channel operation, such as sending or receiving data on a channel.

The select statement then blocks until one of the channel operations is ready, at which point it executes the corresponding case.

Syntax of “select”

The syntax of the select statement in Go is quite straightforward. Here’s the general syntax:

select {
case <-channel1:
   // Code to execute when data is received from channel1
case data <-channel2:
   // Code to execute when data is received from channel2
case channel3 <- value:
   // Code to execute when data is sent to channel3
default:
   // Code to execute when no channel operations are ready
}

Explanation of each part of the syntax:

  • select: The keyword used to start a select statement.
  • case <-channel1: Represents a case where data is received from channel1.It can also be written as case data:= <-channel1: if you want to receive and assign the received data to a variable data.
  • case data := <-channel2: Similar to the previous case, but with the assignment of received data to a variable data from channel2.
  • case channel3 <- value: Represents a case where data value is sent to channel3.
  • default: This is optional and represents the default case when no channel operations are ready. It is executed if all other cases are not ready to proceed without blocking.

Blocking & Non-Blocking Operations

In Golang, By default, the basic sends and receives on channels are blocking operations. This means that for unbuffered channels:

  • Sending on a Channel: A goroutine gets blocked until a different goroutine is prepared to accept the value it sent across the channel.
  • Receiving from a Channel: A goroutine gets blocked until another goroutine delivers a value to the channel.

This blocking behavior ensures synchronization between goroutines. However, you can use a select statement to handle channel operations in a non-blocking way. Here’s a brief example to illustrate this:

Blocking Examples

func main() {
   ch := make(chan int)
     go func() {
       ch <- 12   }()
   integerValue := <-ch
   fmt.Println(integerValue)
}
// output : 12

In the above example, the send and receive operations block execution until they are paired with a corresponding receive and send operation respectively.

Non-Blocking Example

func main() {
   ch := make(chan int)
      go func() {
       ch <- 42
   }()
     select {
   case value := <-ch:
       fmt.Println("Value is received:", value)
   default:
       fmt.Println("Value is not received")
   }
}
// output :  Value is not received

The “select” statement is trying to retrieve a value from the channel in the above example. The “default” case is executed when the selected statement does not receive value from the channel operation. It is a best practice to put the “Default” case in a “select” statement when performing nonblocking channel operations within Golang.

For the complete program, you can visit my GitHub repository

Timeouts in “select”

You can use the “select” statement to wait on multiple channel operations. When the “select” statement tries to retrieve the value from the multiple channels operation and “select” does not get any value, that time you can use timeouts. For using the timeouts, you can use “time. After()” function, by specifying a timeout duration after which a case in the “select” statement will execute. To demonstrate how to use the “select” statement to implement a timeout, here is an example:

func main() {

   taskChannel := make(chan string, 1)

   go func() {
       time.Sleep(2 * time.Second)
       taskChannel <- "Task completed"
   }()

   select {
   case msg := <-taskChannel:
       fmt.Println(msg)
   case <-time.After(1 * time.Second):
       fmt.Println("Timeout: Task took too long")
   }
}
// output: Timeout: Task took too long

In the above example:

  1. We create a taskChannel to simulate a task.
  2. We launch a goroutine that sleeps for 2 seconds and then sends a message to taskChannel.
  3. We use a select statement to wait for either the message from taskChannel or a timeout specified by time.After(1 * time.Second).
  4. If the task is completed within 1 second, the message from taskChannel is printed.
  5. If the task takes longer than 1 second, the timeout case executes, and “Timeout: Task took too long” is printed.

Explanation:

  • select waits for one of its cases to proceed. If multiple cases are ready to proceed, one is chosen at random.
  • time.After(d) returns a channel that sends the current time after a duration d.
  • This pattern is useful for ensuring that your program does not hang indefinitely waiting for a channel operation to complete.

By using select with time.After, you can handle timeouts gracefully and ensure that your program remains responsive even when dealing with potentially long-running operations. For the complete program, you can visit my GitHub repository.

“default” case in “select”

If no other cases are ready to process, the default case in the select statement will execute the code. This may help to implement non-blocking operations. The use of the default case in a “select” statement is shown in the examples below:

func main() {
   channelOne := make(chan string)
   channelTwo := make(chan string)

   go func() {
       time.Sleep(2 * time.Second)
       channelOne <- "Message from channelOne"
   }()

   go func() {
       time.Sleep(1 * time.Second)
       channelTwo <- "Message from channelTwo"
   }()

   for {
       select {
       case messageOne := <-channelOne:
           fmt.Println(messageOne)
           return
       case messageTwo := <-channelTwo:
           fmt.Println(messageTwo)
           return
       default:
           fmt.Println("Waiting for the messages...")
           time.Sleep(500 * time.Millisecond)
       }
   }
}

In the above example:

  1. Two channels, channelOne and channelTwo, are created.
  2. Two goroutines are spawned: one sends a message to channelOne after 2 seconds, and the other sends a message to channelTwo after 1 second.
  3. A for loop is used to continuously check the channels.
  4. The select statement inside the loop has three cases:
    • If there is a message on channelOne, it prints the message and returns.
    • If there is a message on channelTwo, it prints the message and returns.
    • If neither channel has a message, the default case executes, printing a message indicating that no messages have been received yet and sleeping for 500 milliseconds before the next iteration.

For the complete program, you can visit the GitHub repository

Summary

  1. A “select” statement blocks until at least one of its cases can proceed.
  2. The “default” case prevents deadlocks in the “select” statement. If the “select” statement does not use the “default” case in it and channel operations do not get any data, resulting in crashing the program
  3. A “select” statement can handle only one case.
  4. A “select” statement will wait for a channel to receive data if no default case is specified.