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
orchannel2
) receives data and proceeds to the correspondingcase
. It will choose a randomcase
to proceed if multiple channels are ready at the same time. - If none of the channels are ready, the
switch
statement will execute thedefault
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 ch2
Code 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.