Select statement

12 minutes read

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: 505

Be 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 select statement 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 select statement will either:

    • Cause the current executing goroutine to block, waiting until at least one of the channels becomes ready for communication, or

    • Execute the default branch 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 employees

In 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 select statement can be used for reading/writing operations on channels.

  • The select statement selects the operation that can be executed (if there are several such operations, one is selected randomly).

  • The operations defined in the select statement are executed immediately (remember this if you need to combine the select statement with time-based operations).

  • The select statement can be used to block a goroutine (select {}).

5 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo