Go Select

Summary: in this tutorial, you will learn how to use the select statement to manage multiple channel operations.

Introduction to the Go select statement

Goroutines communicate with each other via channels. They share data via communication.

In Go, the select statement allows a goroutine to wait on multiple channels, proceeding with the first available channel. The select statement works like a switch statement but for channels.

Here’s the basic syntax of the select statement:

select {
case <-channel1:
    // Code to execute when channel1 receives data
case data := <-channel2:
    // Code to execute when channel2 receives data
default:
    // Code to execute if none of the above channels are ready
}Code language: Go (go)

In this syntax:

  • Each case statement listens to a channel.
  • The select statement waits until a channel (channel1 or channel2) receives data and proceeds to the corresponding case. It will choose a random case to proceed if multiple channels are ready at the same time.
  • If none of the channels are ready, the switch statement will execute the default branch.

A basic Go select statement example

The following example illustrates how the select statement works:

package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("The program started...")

    ch1 := make(chan string)
    ch2 := make(chan string)

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

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

    select {
    case msg1 := <-ch1:
        fmt.Println(msg1)
    case msg2 := <-ch2:
        fmt.Println(msg2)
    }
}Code language: Go (go)

Output:

The program started...
Message from ch2Code language: Go (go)

How it works.

First, create two channels with the type string:

ch1 := make(chan string)
ch2 := make(chan string)Code language: Go (go)

Second, spawn a goroutine that delays 2 seconds before sending the message "Message from channel 1" to channel 1 (ch1):

go func() {
    time.Sleep(2 * time.Second)
    ch1 <- "Message from channel 1"
}()Code language: Go (go)

Third, spawn a second goroutine that takes 1 second before sending the message "Message from channel 2" to channel 2 (ch2):

go func() {
    time.Sleep(1 * time.Second)
    ch2 <- "Message from channel 2"
}()Code language: Go (go)

Finally, use the select statement to wait for the first available channel and display the received message:

select {
case msg1 := <-ch1:
    fmt.Println(msg1)
case msg2 := <-ch2:
    fmt.Println(msg2)
}Code language: Go (go)

The second goroutine sends data to the ch2 channel before the first goroutine sends data to the ch1 channel.

Therefore, the channel ch2 is ready first, which executes the second case in the switch statement.

After executing the second case, the switch statement exists. Meanwhile, the first goroutine is running until the main goroutine ends.

A practical Go select statement example

The following example shows how to download two files from a remote server concurrently and store the one that is first available:

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
)

type Response struct {
    data []byte
    err  error
}

func fetch(url string, ch chan Response) {
    resp, err := http.Get(url)
    if err != nil {
        ch <- Response{data: nil, err: err}
        return
    }

    defer resp.Body.Close()

    bs, _ := io.ReadAll(resp.Body)
    ch <- Response{data: bs, err: nil}
}

func save(filepath string, data []byte) error {
    out, err := os.Create(filepath)
    if err != nil {
        return fmt.Errorf("failed to create file: %v", err)
    }
    defer out.Close()

    _, err = out.Write(data)
    if err != nil {
        return fmt.Errorf("failed to close file: %v", err)
    }
    return nil
}

func process(resp Response, filepath string) {
    if resp.err != nil {
        fmt.Println("Error:", resp.err)
        return
    }
    err := save(filepath, resp.data)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
}

func main() {
    urls := []string{
        "https://www.rfc-editor.org/rfc/rfc1543.txt",
        "https://www.rfc-editor.org/rfc/rfc1541.txt",
    }

    ch1 := make(chan Response)
    ch2 := make(chan Response)

    go fetch(urls[0], ch1)
    go fetch(urls[1], ch2)

    select {
    case resp := <-ch1:
        process(resp, "rfc1543.txt")
    case resp := <-ch2:
        process(resp, "rfc1541.txt")
    }
}Code language: Go (go)

How it works.

Step 1. Define a Response struct that includes two fields data and err.

type Response struct {
    data []byte
    err  error
}Code language: Go (go)

Step 2. Define a fetch() function that downloads data from a URL and sends the data to a channel with the type of Response:

func fetch(url string, ch chan Response) {
    resp, err := http.Get(url)
    if err != nil {
        ch <- Response{data: nil, err: err}
        return
    }

    defer resp.Body.Close()

    bs, _ := io.ReadAll(resp.Body)
    ch <- Response{data: bs, err: nil}
}Code language: Go (go)

Step 3. Define the save() function that saves the downloaded data into a file:

func save(filepath string, data []byte) error {
    out, err := os.Create(filepath)
    if err != nil {
        return fmt.Errorf("failed to create file: %v", err)
    }
    defer out.Close()

    _, err = out.Write(data)
    if err != nil {
        return fmt.Errorf("failed to close file: %v", err)
    }
    return nil
}Code language: Go (go)

Step 4. Define the process() function that processes the Response and saves the data field from it to a file:

func process(resp Response, filepath string) {
    if resp.err != nil {
        fmt.Println("Error:", resp.err)
        return
    }
    err := save(filepath, resp.data)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
}Code language: Go (go)

Step 5. Define the main function:

func main() {
    urls := []string{
        "https://www.rfc-editor.org/rfc/rfc1543.txt",
        "https://www.rfc-editor.org/rfc/rfc1541.txt",
    }

    ch1 := make(chan Response)
    ch2 := make(chan Response)

    go fetch(urls[0], ch1)
    go fetch(urls[1], ch2)

    select {
    case resp := <-ch1:
        process(resp, "rfc1543.txt")
    case resp := <-ch2:
        process(resp, "rfc1541.txt")
    }
}Code language: Go (go)

How it works.

First, initialize a slice of URLs to download:

urls := []string{
    "https://www.rfc-editor.org/rfc/rfc1543.txt",
    "https://www.rfc-editor.org/rfc/rfc1541.txt",
}Code language: Go (go)

Second, create two channels with the type Response:

ch1 := make(chan Response)
ch2 := make(chan Response)Code language: Go (go)

Third, spawn two goroutines to download data from each URL in the urls slice and send that data to the corresponding channel:

go fetch(urls[0], ch1)
go fetch(urls[1], ch2)Code language: Go (go)

Finally, use the select statement to wait for the response from either channel ch1 or ch2:

select {
case resp := <-ch1:
    process(resp, "rfc1543.txt")
case resp := <-ch2:
    process(resp, "rfc1541.txt")
}Code language: Go (go)

Once a channel receives data, the process() function will execute which calls the save() function to save data to a file.

Timeout

The following program illustrates how to timeout an operation if it takes more than 2 seconds to complete:

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan string)

    go func() {
        time.Sleep(3 * time.Second)
        ch <- "Operation completed"
    }()

    select {
    case msg := <-ch:
        fmt.Println(msg)
    case <-time.After(2 * time.Second):
        fmt.Println("The operation took too long.")
    }
}Code language: Go (go)

Output:

The operation took too long.Code language: Go (go)

How it works.

First, create a new channel:

ch := make(chan string)Code language: Go (go)

Second, spawn a new goroutine that takes 3 seconds before sending a message to the channel:

go func() {
    time.Sleep(3 * time.Second)
    ch <- "Operation completed"
}()Code language: Go (go)

Third, use the select statement to wait for channels:

select {
case msg := <-ch:
    fmt.Println(msg)
case <-time.After(2 * time.Second):
    fmt.Println("The operation took too long.")
}Code language: Go (go)

The time.After(2 * time.Second) creates a channel that sends the current time after 2 seconds.

Since it takes 3 seconds for the data to be available on channel ch, the second case executes and displays the message "The operation took too long.".

Using for with select statement

The select statement processes the first available channel and exits. To process data from multiple channels with various logic, you can use a for loop:

for {
    select {
    case msg1 := <-channel1:
        // Code to execute when channel1 receives data
    case msg2 := <-channel2:
        // Code to execute when channel2 receives data
    default:
        // Code to execute if none of the above channels are ready
    }
}Code language: Go (go)

How it works.

  • First, use the for statement to create an indefinite loop.
  • Second, use the select statement to wait for data from multiple channels and process data from channels in sequence.

For example:

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

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

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

    messages := []string{}

Loop:
    for {
        select {
        case msg1 := <-ch1:
            messages = append(messages, msg1)
        case msg2 := <-ch2:
            messages = append(messages, msg2)
        default:
            if len(messages) == 2 {
                break Loop
            }
            fmt.Println("Waiting for message...")
            time.Sleep(500 * time.Millisecond)
        }
    }
    fmt.Println(messages)
}Code language: Go (go)

Output:

Waiting for message...
Waiting for message...
Waiting for message...
Waiting for message...
[Message from channel 1 Message from channel 2]Code language: Go (go)

How it works.

First, create two channels:

ch1 := make(chan string)
ch2 := make(chan string)Code language: Go (go)

Second, spawn two goroutines, each sending a message to the corresponding channel:

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

go func() {
    time.Sleep(2 * time.Second)
    ch2 <- "Message from channel 2"
}()Code language: Go (go)

The first goroutine takes 1 second while the second go routine takes 2 seconds to finish.

Third, initialize a slice that stores the messages received from channels:

messages := []string{}Code language: Go (go)

Fourth, use the for and select statements to wait for messages from the channels ch1 and ch2:

Loop:
    for {
        select {
        case msg1 := <-ch1:
            messages = append(messages, msg1)
        case msg2 := <-ch2:
            messages = append(messages, msg2)
        default:
            if len(messages) == 2 {
                break Loop
            }
            fmt.Println("Waiting for message...")
            time.Sleep(500 * time.Millisecond)
        }
    }Code language: Go (go)

The for loop has a label Loop to terminate the loop inside the switch statement.

The first two cases wait for data to be available on channels ch1 and ch2. Once the data is available, they append it to the messages slice.

The default branch executes when the data is not available on any channel. It displays the message "Waiting for message..." and pauses 500 ms.

Additionally, the default branch will terminate the loop if the data is available on all channels, by checking the size of the messages slice.

Summary

  • Use the select statement to handle multiple channel operations.
Was this tutorial helpful?