Select Notions You Should Know in Golang

Mastering Go’s Select: Beyond the Basics
golang select (from github.com/MariaLetta/free-gophers-pack)|300

Medium Link: Select Notions You Should Know in Golang | by Wesley Wei | Sep, 2024 | Programmer’s Career
Author:Wesley Wei – LinkedIn, Wesley Wei – Medium, Wesley Wei – Twitter

Hello, here is Wesley, Today’s article is about select in Go. Without further ado, let’s get started.💪

1.1 Select Introduction

In the Go language, the select statement is used to handle multiple channel operations and simplify concurrent programming’s communication and synchronization issues. The select statement is similar to the switch statement, but each case must be a channel operation.

Here is the basic syntax of the select statement:

1
2
3
4
5
6
7
8
select {
case <-chan1:
// Executed when chan1 has data to receive
case chan2 <- value:
// Executed when data can be sent to chan2
default:
// Executed when none of the cases are satisfied
}

Use Cases

  • Multiple Channel Selection: You can wait for multiple channel operations simultaneously, and when any one of them is ready, the program will execute the corresponding case.
  • Timeout Handling: By combining with the time.After function, you can implement a timeout mechanism.
  • Non-blocking Communication: By using the default clause, you can achieve non-blocking channel operations.

Regarding timeout handling, please refer to the article: Time Control You Should Know in Golang | by Wesley Wei | Programmer’s Career

1.2 Select and Channels

Unlike switch, although select also has multiple cases, these cases must be related to channel operations (i.e., read-write operations). It can help developers avoid deadlocks caused by waiting for a blocked channel.

Example 1: Receiving Data from Multiple Channels

Here is an example that shows how to use the select statement to receive data from two channels:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"fmt"
"time"
)

func main() {
chan1 := make(chan string)
chan2 := make(chan string)

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

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

select {
case msg1 := <-chan1:
fmt.Println(msg1)
case msg2 := <-chan2:
fmt.Println(msg2)
}
}

In this example, we create two channels chan1 and chan2, and through two goroutines, send messages to these channels at different times. The select statement will wait for the first channel to be ready and receive a message, then output the corresponding information.

Example 2: Handling Timeouts

Below is an example that demonstrates how to use the select statement with a timeout mechanism to handle multiple channel operations:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main
import (
"fmt"
"time"
)
func main() {
chan1 := make(chan string)
chan2 := make(chan string)

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

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

select {
case msg1 := <-chan1:
fmt.Println(msg1)
case msg2 := <-chan2:
fmt.Println(msg2)
case <-time.After(1 * time.Second):
fmt.Println("Timeout, no channel was ready within 1 second")
}
}

In this example, the select statement waits for data to be read from channels chan1 and chan2. If more than 1 second has passed without any channel being ready, the select statement will execute the third case, printing “Timeout, no channel was ready within 1 second”.

Example 3: Non-Blocking Communication

Finally, let’s look at an example that demonstrates how to use a default clause to implement non-blocking channel operations:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import (
"fmt"
)
func main() {
chan1 := make(chan string)
select {
case msg := <-chan1:
fmt.Println(msg)
default:
fmt.Println("Default case: Channel is not ready")
}
}

In this example, if channel chan1 has no data to receive, the select statement will execute the default clause, printing “Default case: Channel is not ready”.

1.3 Implementation of select

1.3.1 Data Structure

There is no corresponding struct in Go’s source code for select, but we use the runtime.scase struct to represent the case structure within the select control structure:

1
2
3
4
type scase struct {
c *hchan // chan
elem unsafe.Pointer // data element
}

Since non-default cases all relate to Channel sending and receiving, the runtime.scase struct also contains a  runtime.hchan  type field to store the Channel used in each case.

1.3.2 Common Flow

In the default situation, the compiler will use the following flow to handle the select statement:

  1. Convert all cases into runtime.scase structures containing Channel and type information;
  2. Call the runtime function runtime.selectgo  to select one executable runtime.scasestructure from multiple ready Channels;
  3. Generate a set of if statements using a for loop, which judges whether oneself is the selected case.

1.3.3 Random Polling and Locking Order

The runtime.selectgo  function first performs the necessary initialization operations and determines the two orders for handling cases: polling order pollOrder and locking order lockOrder:

The polling order pollOrder and locking order lockOrder are determined by the following methods:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
// Casts the `cas0` pointer to an array of scase (channel cases)
cas1 := (*[1 << 16]scase)(unsafe.Pointer(cas0))
// Casts the `order0` pointer to an array of uint16 values for polling order
order1 := (*[1 << 17]uint16)(unsafe.Pointer(order0))

// Calculates the total number of cases (sending + receiving channels)
ncases := nsends + nrecvs
// Creates slices for the cases and polling/locking orders
scases := cas1[:ncases:ncases]
pollorder := order1[:ncases:ncases]
lockorder := order1[ncases:][:ncases:ncases]

norder := 0
// Iterates through each channel case
for i := range scases {
cas := &scases[i] // Retrieves a pointer to the current scase (channel case)

j := cheaprandn(uint32(norder + 1)) // Generates a random number up to i
pollorder[norder] = pollorder[j] // Swaps current poll order with a random one
pollorder[j] = uint16(i)
norder++
}
// Trims the pollorder and lockorder slices to the number of cases
pollorder = pollorder[:norder]
lockorder = lockorder[:norder]

// Sorts the lock order based on channel addresses to avoid deadlocks

sellock(scases, lockorder) // Locks the channels in the determined order

}

func sellock(scases []scase, lockorder []uint16) {
var c *hchan
for _, o := range lockorder {
c0 := scases[o].c
if c0 != c {
c = c0
lock(&c.lock)
}
}
}
  • Polling order: introduces randomness using the  runtime.cheaprandn  function;
  • Locking order: sorts Channels based on their addresses to avoid deadlocks.

Random polling can prevent Channel starvation and ensure fairness, while sorting Channels by address can prevent deadlock occurrence. This code finally calls the  runtime.sellockfunction to lock all Channels in the determined order.

1.4 Performance Considerations:

  1. Registration and Selection Overhead
    When a select statement is executed, the Go runtime needs to register each channel operation with the scheduler queue and dequeue it when the channel becomes available. For each select statement, the registration and selection process incurs additional scheduling overhead.

  2. Random Selection Overhead
    When multiple channels are ready simultaneously, the Go runtime will randomly select one channel to execute. This random selection involves generating a random number and scanning all ready channels, which introduces additional overhead.

  3. Blocking and Waking Up Overhead
    When select blocks waiting for a channel, the Go runtime needs to put the current goroutine into the wait queue until the channel has data available. The blocking and waking up process involves scheduling goroutines, which also incurs some context switch overhead.

1.5 I/O Multiplexing:

After analyzing the above, you may have a familiar feeling; no mistake, I also thought of I/O multiplexing. In operating systems, select is a system call, and we often use select, poll, and epoll functions to build an I/O multiplexing model to improve program performance.

The Go language’s select is similar to the system call in operating systems, such as C’s select system call, which can simultaneously listen for multiple file descriptors’ readability or writability. Similarly, Go’s select allows goroutines to simultaneously wait for multiple channels to be readable or writable.

Their abstract form is similar to the following diagram:

IO Multiplexing

1.6 Conclusion:

  • On the surface:
    Go’s select statement is a specialized statement only used for sending and receiving messages on channels, which runs blocking; when there are no case statements in select, it will block the current goroutine. Therefore, select is used to block listening to goroutines.

  • Deeper understanding:
    select is Go’s mechanism for I/O multiplexing at the language level, specifically designed to detect whether multiple readable or writable channels are ready.

1.7 References

[1] Time Control You Should Know in Golang | by Wesley Wei | Programmer’s Career: https://medium.programmerscareer.com/time-control-you-should-know-in-golang-9699de89eece
[2] runtime.scase: https://cs.opensource.google/go/go/+/refs/tags/go1.23.0:src/runtime/select.go
[4] runtime.hchan: https://cs.opensource.google/go/go/+/release-branch.go1.23:src/runtime/chan.go;l=33
[6] runtime.selectgo: https://cs.opensource.google/go/go/+/release-branch.go1.23:src/runtime/select.go;l=121
[9] runtime.cheaprandn: https://cs.opensource.google/go/go/+/release-branch.go1.23:src/runtime/rand.go;l=255
[10] runtime.sellock: https://cs.opensource.google/go/go/+/release-branch.go1.23:src/runtime/select.go;l=33
[11] Go 语言 select 的实现原理 | Go 语言设计与实现: https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-select


More Series Articles about You Should Know In Golang:

https://wesley-wei.medium.com/list/you-should-know-in-golang-e9491363cd9a

And I’m Wesley, delighted to share knowledge from the world of programming. 

Don’t forget to follow me for more informative content, or feel free to share this with others who may also find it beneficial. it would be a great help to me.

Give me some free applauds, highlights, or replies, and I’ll pay attention to those reactions, which will determine whether or not I continue to post this type of article.

See you in the next article. 👋

中文文章: https://programmerscareer.com/zh-cn/golang-select/
Author: Wesley Wei – Twitter Wesley Wei – Medium
Note: Originally written at https://programmerscareer.com/golang-select/ at 2024-09-22 14:59.
Copyright: BY-NC-ND 3.0

SyncOnce You Should Know in Golang Defer You Should Know in Golang(Fundamental Applications)

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×