This article reviews some of the uses of the select statement in the Go language and extends a tip on how to implement priority in select, which I hope will be helpful to you.

Introduction to the select statement

The select statement in Go is used to monitor and select a set of case statements to execute the corresponding code. It looks similar to the switch statement, but all the expressions in the case in the select statement must be send or receive operations of the channel. An example of a typical use of select is as follows.

1
2
3
4
5
6
select {
case <-ch1:
	fmt.Println("liwenzhou.com")
case ch2 <- 1:
	fmt.Println("q1mi")
}

The select keyword in the go language also allows the current goroutine to wait for ch1 to become readable and ch2 to become writable at the same time, and until the state of ch1 and ch2 changes, select will block until one of the channels becomes ready and the corresponding case branch. If more than one channel is ready at the same time, then a case is randomly selected for execution.

In addition to the typical examples shown above, we will introduce some special examples of select one by one.

Empty select

An empty select means that it does not contain any case inside, e.g.

1
2
3
select{
  
}

An empty select statement will directly block the current goroutine, causing it to go into a permanently dormant state where it cannot be woken up.

Only one case

If select contains only one case, then the select becomes a blocking channel read/write operation.

1
2
3
4
select {
case <-ch1:
	fmt.Println("liwenzhou.com")
}

In the above code, the print operation is executed when ch1 is readable, otherwise it blocks.

With default statement

If select can also contain a default statement, it can be used to perform some default operation when none of the other cases are satisfied.

1
2
3
4
5
6
select {
case <-ch1:
	fmt.Println("liwenzhou.com")
default:
	time.Sleep(time.Second)
}

In the above code, the print operation is executed when ch1 is readable, otherwise the code in the default statement is executed, which is equivalent to doing a non-blocking channel read operation.

Summary

  • select No case exists: permanently blocks the current goroutine
  • select Only one case exists: blocking send/receive
  • select exists more than one case: randomly select a case that satisfies the condition and execute it
  • select exists default and no other case is satisfied: execute the code in the default statement

Implementing priority in select

It is known that when there are multiple cases in select, one case satisfying the condition is randomly selected for execution.

Now we have a requirement: we have a function that will continuously receive task 1 and task 2 from ch1 and ch2 respectively without interruption.

How can we ensure that when ch1 and ch2 reach the ready state at the same time, task 1 will be executed first, and task 2 will be executed when there is no task 1?

Advanced Go programmer Ming scratched his head and wrote the following function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func worker(ch1, ch2 <-chan int, stopCh chan struct{}) {

	for {
		select {
		case <-stopCh:
			return
		case job1 := <-ch1:
			fmt.Println(job1)
		default:
			select {
			case job2 := <-ch2:
				fmt.Println(job2)
			default:
			}
		}
	}
}

The above code implements “priority” by nesting two selects, which seems to meet the requirements of the question. But there is something wrong with this code, if both ch1 and ch2 do not reach the ready state, the whole program will not block but enter a dead loop.

What can we do?

Xiao Ming scratched his head again and wrote down another solution.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func worker2(ch1, ch2 <-chan int, stopCh chan struct{}) {
	for {
		select {
		case <-stopCh:
			return
		case job1 := <-ch1:
			fmt.Println(job1)
		case job2 := <-ch2:
		priority:
			for {
				select {
				case job1 := <-ch1:
					fmt.Println(job1)
				default:
					break priority
				}
			}
			fmt.Println(job2)
		}
	}
}

This time, Ming not only uses nested select, but also a combination of for loops and LABEL to achieve the requirements of the question. The above code executes job2 := <-ch2 when the outer select is selected, and then goes to the inner select loop to continue trying to execute job1 := <-ch1, which will keep executing when ch1 is ready, otherwise it jumps out of the inner select.

Practical application scenarios

Although the above requirements are compiled by me, but about the implementation of priority in select in the actual production is a practical application of the scene, such as K8s controller in the actual use of the above skills examples, here in the select to achieve priority-related code in the key have added comments, the specific logic here will not expand on the details.

 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
// https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/nodelifecycle/scheduler/taint_manager.go
func (tc *NoExecuteTaintManager) worker(worker int, done func(), stopCh <-chan struct{}) {
	defer done()

	// When processing events we want to prioritize Node updates over Pod updates,
	// as NodeUpdates that interest NoExecuteTaintManager should be handled as soon as possible -
	// we don't want user (or system) to wait until PodUpdate queue is drained before it can
	// start evicting Pods from tainted Nodes.
	for {
		select {
		case <-stopCh:
			return
		case nodeUpdate := <-tc.nodeUpdateChannels[worker]:
			tc.handleNodeUpdate(nodeUpdate)
			tc.nodeUpdateQueue.Done(nodeUpdate)
		case podUpdate := <-tc.podUpdateChannels[worker]:
			// If we found a Pod update we need to empty Node queue first.
		priority:
			for {
				select {
				case nodeUpdate := <-tc.nodeUpdateChannels[worker]:
					tc.handleNodeUpdate(nodeUpdate)
					tc.nodeUpdateQueue.Done(nodeUpdate)
				default:
					break priority
				}
			}
			// After Node queue is emptied we process podUpdate.
			tc.handlePodUpdate(podUpdate)
			tc.podUpdateQueue.Done(podUpdate)
		}
	}
}

Summary

This article reviews some of the uses of the select statement in Go, and extends a tip on how to implement priority in select, which I hope will be helpful to you.

One last word, Go language doesn’t have a lot of weird syntax sugar and self-contained code formatting compared to other languages, so it doesn’t have the situation of not being able to read the code written by others. So we can totally read the source code of the best libraries, be with giants, be with high friends, and eventually eat better meals.


Reference https://www.liwenzhou.com/posts/Go/priority_in_go_select/