select

Select is a multiplex I/O multiplexing mechanism provided by Go at the language level to check whether multiple pipes are ready (readable or writable), and its characteristics are closely related to pipes.

Features quick reference

Pipeline, speaking, reading and writing

Select can only work on pipes, including reading and writing data, as shown in the following code:

func SelectForChan(c chan string) {
  var recv string
  send := "Hello"
  select {
  case recv = <-c:
    fmt.Printf("recvied %s \n", recv)
  case c <- send:
    fmt.Printf("sent %s \n", send)
  }
}
Copy the code

In the above code, SELECT has two case statements, one for the pipe’s read and one for its write, depending on which pipe the function passes.

In the first case, the pipe has no buffer:

func main() {
  c:=make(chan string)
  SelectForChan(c)
}
Copy the code

In this case, the pipe can neither read nor write, neither case statement is executed, and the SELECT is blocked. Fatal error: All goroutines are asleep – deadlock! , exit the program because: On the main Goroutine line, we expect to get a number from the pipe that the other Goroutines put into the pipe, but all the other Goroutines are asleep, so no data is ever put into the pipe. So, the main Goroutine line waits for a data that never comes, and the whole program waits forever. This is obviously fruitless, so the program says, “Forget it, no more persistence. I’ll kill myself and report a mistake to the code author, and I’m set.” In general, this situation should be avoided. Another way is to append the default option.

In the second case, the pipe has a buffer and can hold at least one data:

func main() {
  c:=make(chan string,1)
  SelectForChan(c)
}
Copy the code

The pipe can write data, and the case corresponding to the write operation is executed. After execution, sent Hello is printed, and the function exits.

In the third case, the pipe has a buffer that is already full of data:

func main() {
  c:=make(chan string,1)
  c<-"world"
  SelectForChan(c)
}
Copy the code

At this point, the pipe can read data, and the corresponding case of the read operation is executed. After execution, recvied World is output, and the function exits

In the fourth case, the pipe has a buffer that contains only partial data and can continue to hold data:

func main() {
  c:=make(chan string,2)
  c<-"world"
  SelectForChan(c)
}
Copy the code

In this case, the pipe can be read or written. Select will randomly select a case statement to execute, and the function will exit after the execution of any case statement.

To sum up, each case statement of SELECT can operate on only one pipe, either writing or reading data. Given the pipe’s special effects, if there are no read operations in the pipe it will block, and if there are no free buffers in the pipe it will block write operations. When multiple pipes in a SELECT case are blocked, the entire SELECT statement is blocked (in the absence of a default statement, and not, the main function) until any pipe is unblocked. If multiple cases are not blocked, a random case is selected for execution.

The return value

Select is a reserved key for Go, not a function. It can declare variables and assign values to variables in case statements, and looks like a function.

When a case statement reads a pipe, it can assign a maximum of two variables, as follows:

Func SelectAssign(c chan string) {select {case <-c: // FMT.Println("0") case d := <-c: // FMT.Printf("1: Received %s \n",d) case d, ok := <-c: //2 if! ok{ fmt.Printf("no data found") break } fmt.Printf("2: received %s \n",d) } }Copy the code

The pipe read operation in the case statement has two return conditions, one is successfully read data, and the pipe has no data and has been closed. When a case statement contains two variables, the second variable indicates whether the data was successfully read.

The following code is passed to a closed pipe:

func main() {
  c := make(chan string)
  close(c)
  SelectAssign(c)
}
Copy the code

All three case statements in the SELECT have a chance to execute. Each case statement receives null data, but the third case statement senses that the pipe is closed so that no null data is printed.

default

The dafault statement in SELECT cannot handle pipe reads and writes. When all case statements in select are blocked, the default statement will be executed as follows:

func SelectDefault() {
  c := make(chan string)
  select {
  case <-c:
    fmt.Printf("received %s \n")
  default:
    fmt.Printf("no data found in default \n")
  }
}
Copy the code

Since the pipe has no buffer, the read operation must be blocked, whereas select has a default branch, which will execute the default branch and exit.

In addition, default is actually a special case that can appear anywhere in the select, but there can only be one default per select.

Using an example

Here are some examples of using SELECT in a real project

Permanent congestion

Sometimes we can permanently block main when we start the coroutine processing task and don’t want main to exit.

There are examples of using select to block main in several components of the Kubernetes project, such as the Webhook test component in Apiserver:

func main(){
  server := webhooktesting.NewTestServer(nil)
  server.StartTLS()
  fmt.Println("serving on",server.URL)
  select()
}
Copy the code

The above code does not contain case and default statements in the SELECT statement, and the coroutine (main) is permanently blocked.

A quick check

Sometimes we use pipes to transmit errors, so we can use the SELECT statement to quickly check for errors in the pipe to avoid falling into a loop. For example, the Kubernetes scheduler has a similar usage:

ErrCh := make(chan error,active) jm.deelteJobPods(& Job,activePods, errCh) // The incoming pipe is used to record the error select{case manageJobErr = < -errch: // Check if there is an error if manageJobErr! = nil {break} defalut: // No error, quick end check}Copy the code

The above select is only used to try to read error information from the pipe, and if there are no errors, it will not be blocked.

Time wait for

Sometimes we use pipes to manage the context of a function, and select can be used to create pipes that are time-limited. For example, kubernetes controller has a similar usage:

func waitForStopOrTimeout(stopCh <-chan struct{} , timeout time.Duration) <-chan struct{} { stopChWithTimeout := make(chan struct{}) go func(){ select{ case <-stopCh: Case < time.After(timeout):} close(stopChWithTimeout)}() return stopChWithTimeout}Copy the code

This function returns a pipe that can be used to pass between functions, but the pipe closes automatically after a specified time.

Realize the principle of

Studying the implementation principle of SELEC can help us understand the following issues more clearly:

  • Why can only one pipe be handled per case statement?
  • Why are case statements executed in random order?
  • Why does writing to a nil pipe in a case statement not trigger panic?

The data structure

Select * from scase(select-case);

// Select case descriptor. // Known to compiler. // Changes here must also be made in src/cmd/internal/gc/select.go's Scasetype. type scase struct {c *hchan // chan kind uint16 // After version 1.16, elem unsafe.Pointer // data element}Copy the code
The pipe

Member C in scASE represents the pipe that case statements operate on. Since only one pipe can be stored in each case, this directly determines that only one pipe can be handled in each case statement. In addition, the compiler will give a compilation error if there is no pipe operation in the case statement (which cannot be processed as a scase object) :

select case must be receive , send or assign recv
Copy the code
type

The member Kind in scase represents the type of case statement, each of which represents a type of pipe operation or special case (the member field was removed in go 1.16).

Const (caseNil = iota // pipe value nil caseRecv // read pipe case caseSend // write pipe case caseDefault //default)Copy the code

A case statement of type caseNil indicates that the pipe value of its operation is nil. Since nil pipes are neither readable nor writable, this means that cases of this kind will never hit and will be ignored at run time, which is why writing data to a pipe with a value of nil in a case statement does not trigger panic.

A case statement of type caseRecv that reads data from the pipe

Case statement of type caseSend, indicating that it will write data to the pipe

Default is a special type of case statement that does not manipulate pipes. In addition, only one default statement can exist in each SELECT statement, and the default statement can appear anywhere.

data

The member ELEm in SCase represents the address where the data is stored and has a different meaning depending on the case type

  • In a case of type receive chan, elem represents the address where the data read from the pipe is stored
  • In a case of type write chan, elem represents the location of the data to be written to the pipe

Implementation logic

Go provides the selectGo () function in the Runtime package to handle select statements:

// selectgo implements the select statement.
// cas0 points to an array of type [ncases]scase, and order0 points to
// an array of type [2*ncases]uint16 where ncases must be <= 65536.
// Both reside on the goroutine's stack (regardless of any escaping in
// selectgo).
// For race detector builds, pc0 points to an array of type
// [ncases]uintptr (also on the stack); for other builds, it's set to
// nil.
// selectgo returns the index of the chosen scase, which matches the
// ordinal position of its respective select{recv,send,default} call.
// Also, if the chosen scase was a receive operation, it reports whether
// a value was received.
func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {}
Copy the code

The selectGo () function selects a case from a list of cases and returns the hit subscript, and for cases of type caseRecv, whether data was successfully read from the pipe (the second return value is meaningless for other types of cases).

The implementation of selectGo () includes the following points:

  • The original case order is shuffled by the random function fastrandn(), and the randomness is shown by using the shuffled order when iterating through individual cases.
  • If a case is found ready (pipe-readable or writable) as you loop through each case, you jump out of the loop, pipe and return
  • When iterating through each case, the loop can end normally (no jump), indicating that all cases are not ready, if there is a default statement, default is hit
  • If all cases are missed and there is no default, selectGo () blocks until all pipes are ready and a new loop is started

summary

  • Select can only operate on pipes
  • Each case statement can handle only one pipe, either read or write
  • Multiple case statements are executed in a random order
  • Select will not be blocked when there is a default statement

Finally, when using SELECT to read a pipe, try to check for success so that pipe exceptions can be found in a timely manner.