Jida's Blog

Concurrency in Go (3): select

25th December 2024
Golang
Concurrency
Goroutine
Last updated:25th January 2025
3 Minutes
439 Words

Intro

While channels are the glue that binds goroutines together, the select statement is the glue that binds channels together. The select statement is undoubtedly one of the most critical things in a Go program with concurrency.

The select statement help safely bring channels together with concepts like cancellations, timeout, waiting, and default values.

Concepts

The syntax of select block is a bit like switch block. However, unlike switch blocks where case statements in a select block are tested sequentially, in select block, the execution won’t fall through if none of the criteria are met. That is saying if none of the channels are ready, the entire select statement blocks until one of the channels is ready and the corresponding statements will execute.

1
func simpleSelectExample() {
2
ch := make(chan any)
3
4
start := time.Now()
5
go func() {
6
time.Sleep(3 * time.Second)
7
// close channel after 3 seconds
8
close(ch)
9
}()
10
11
fmt.Printf("Blocking...\n")
12
select {
13
//blocked until goroutine signals close
14
case <-ch:
15
fmt.Printf("Unblocked %.2v later!\n", time.Since(start))
2 collapsed lines
16
}
17
}
Terminal window
1
# outputs:
2
Blocking...
3
Unblocked 3. later!

Delve deeper

TR;TR

  1. The Go runtime performs a pseudorandom uniform select over the set of case statements, meaning each case statement has an equal chance of being selected.
  2. Add timeout in the case clause to prevent forever blocking.
  3. If we want to process somthing when no channels are ready, have the default clause and put the select block in a for loop (for-select loop).

1) what if multiple channels have something to read?

Each clause of the select statement has the equal chance to be selected. Here is an example:

1
func selectEqualChanceExample() {
2
c1 := make(chan interface{})
3
close(c1)
4
c2 := make(chan interface{})
5
close(c2)
6
7
var c1Count, c2Count int
8
for i := 1000; i >= 0; i-- {
9
select {
10
case <-c1:
11
c1Count++
12
case <-c2:
13
c2Count++
14
}
15
}
2 collapsed lines
16
fmt.Printf("c1Count: %d\nc2Count: %d\n", c1Count, c2Count)
17
}
Terminal window
1
# outputs:
2
c1Count: 508
3
c2Count: 493

As can be seen from the result, the two clauses roughly have the chance to be executed.

2) what if all channels would never be ready?

Use a timeout clause in select statement. Here is an example:

1
func selectTimeoutExample() {
2
now := time.Now()
3
ch1 := make(chan any)
4
5
select {
6
case <-ch1:
7
case <-time.After(2 * time.Second):
8
fmt.Printf("Timed out after %.2v seconds\n", time.Since(now))
9
}
10
}
Terminal window
1
# outputs:
2
Timed out after 2. seconds

3) what if we want to do something when no channel is ready?

Use default clauses if you want to do something when all channels you are selecting against re blocking. Here is an example:

1
func selectDefaultExample() {
2
ch := make(chan any)
3
counter := 0
4
5
go func() {
6
time.Sleep(4 * time.Second)
7
close(ch)
8
}()
9
10
loop:
11
for {
12
select {
13
case <-ch:
14
break loop
15
default:
6 collapsed lines
16
}
17
counter++
18
time.Sleep(1 * time.Second)
19
}
20
fmt.Println("counter:", counter)
21
}
Terminal window
1
# outputs:
2
counter: 4

To see the full example of code, you can visit: goSelect.

Article title:Concurrency in Go (3): select
Article author:Jida-Li
Release time:25th December 2024