Golang’s SELECT statement is used to listen for read/write operations on multiple channels. When a channel read/write operation occurs, it triggers the execution of the case. In actual use, the following points need to be paid attention to.

The for-select loop exits

In a normal for loop, if we want to exit the loop, we can use the break syntax to exit the loop, and if we want to ignore the local loop and continue the next iteration, we can do this by continuing, but in selet’s for loop, Continue again ignores the local loop and continues with the next loop. But break just breaks out of the select and continues the next for loop.

func TestForSelect(a) {
    var i int
    ticker := time.NewTicker(1 * time.Second)
    for {
        select {
        case <-ticker.C:
            i++
            if i == 3 {
                continue
            }
        }
        fmt.Println(i)
    }
    fmt.Println("Out of the for loop")}// Print the result
1
2
4
5.func TestForSelect(a) {
    var i int
    ticker := time.NewTicker(1 * time.Second)
    for {
        select {
        case <-ticker.C:
            i++
            if i == 3 {
                break
            }
        }
        fmt.Println(i)
    }
    fmt.Println("Out of the for loop")}// Print the result
1
2
3
4
5.Copy the code

To exit the for select loop, do the following: Break the label. Note that the label is written before and next to the for loop:

func TestForSelect(a) {
    var i int
    ticker := time.NewTicker(1 * time.Second)
Loop:
    for {
        select {
        case <-ticker.C:
            i++
            if i == 3 {
                break Loop
            }
        }
        fmt.Println(i)
    }
    fmt.Println("Out of the for loop")}// Print the result
1
2
forOutside the circleCopy the code

Jumps to the specified position by goto + label, which needs to come after the for loop.

func TestForSelect(a) {
    var i int
    ticker := time.NewTicker(1 * time.Second)
    for {
        select {
        case <-ticker.C:
            i++
            if i == 3 {
                goto Exit
            }
        }
        fmt.Println(i)
    }
Exit:
    fmt.Println("Out of the for loop")}// Print the result
1
2
forOutside the circleCopy the code

Of course, the most direct way out of the loop is to terminate the function by calling a return in case. This works when there is no more logic to execute after the for loop.

func TestForSelect(a) {
    var i int
    ticker := time.NewTicker(1 * time.Second)
    for {
        select {
        case <-ticker.C:
            i++
            if i == 3 {
                return
            }
        }
        fmt.Println(i)
    }
    fmt.Println("Out of the for loop")}// Print the result
1
2
Copy the code

Select case execution order

As we know, the execution sequence of ordinary Switch cases is from top to bottom. If one case meets the condition, the subsequent cases will not be executed. Otherwise, if none of the conditions are met, If there is a default case, the default case is executed. We can see from the following example that the program determines from top to bottom that case1 meets the condition and then executes the logic of case1.

func main(a) {

    switch {
    case left(0) == right(0):
        fmt.Println("case1 run")
    case left(1) == right(1):
        fmt.Println("case2 run")}}func left(i int) int {
    fmt.Println("left:", i)
    return i
}

func right(i int) int {
    fmt.Println("right:", i)
    return i
}

// Print the result:
left: 0
right: 0
case1 run
Copy the code

If there are multiple “or” statements in an if statement, as long as the previous one matches the condition, the following condition is not performed.

func main(a) {
    if left(0) == right(0) || left(1) == right(1) {
        fmt.Println("test")}}func left(i int) int {
    fmt.Println("left:", i)
    return i
}

func right(i int) int {
    fmt.Println("right:", i)
    return i
}

// Print the result
left: 0
right: 0
test
Copy the code

These cases will be executed from top to bottom and from left to right. If there are multiple cases that meet the channel readiness condition at the same time, one case will be randomly selected for execution. We can verify this by using the following example that conditions in multiple cases are executed once.

func main(a) {

    ch1 := make(chan int.1)
    ch2 := make(chan int.1)
    chs := []chan int{ch1, ch2}

    select {
    case left(0, chs) <- right(0):
        fmt.Println("case1 run")
    case left(1, chs) <- right(1):
        fmt.Println("case2 run")}}func left(i int, chs []chan int) chan int {
    fmt.Println("left:", i)
    return chs[i]
}

func right(i int) int {
    fmt.Println("right:", i)
    return i
}

// Print the result:
left: 0
right: 0
left: 1
right: 1
case1 run // This line of code randomly prints case1 run and case2 run
Copy the code

We need to note that if the select case is a function operation, we will wait until all function operations of these cases are returned before randomly selecting cases that satisfy channel readiness. Let’s look at the following example: select listens for read operations on three channels. Case1 and case2 calls return channel immediately. The difference is that Case1 enables Groutine to write data to the channel after 50ms, and CasE2 to write data to the channel after 200ms. Case3 calls AsyncCall2 and returns a readable channel after 3000ms.

func AsyncCall(t int) <-chan int {
    c := make(chan int)
    go func(a) {
        time.Sleep(time.Millisecond * time.Duration(t))
        c <- t
    }()
    return c
}

func AsyncCall2(t int) <-chan int {
    c := make(chan int)
    go func(a) {
        c <- t
    }()
    time.Sleep(3000 * time.Millisecond)
    return c
}

func main(a) {
    select {
    case resp := <-AsyncCall(50):
        fmt.Println(resp)
    case resp := <-AsyncCall(200):
        fmt.Println(resp)
    case resp := <-AsyncCall2(3000):
        fmt.Println(resp)
    }
}

// Print the resultWaiting for the3000Ms is random after50,200,3000Print a value inCopy the code

Many people may think that the above code will print 50 at 50 ms because they think the first case is the fastest, but this perception is wrong. The correct result is that the program will randomly print a value in 50, 200, 3000 after 3000ms. The reason is that when the SELECT statement needs to read or write from multiple channels, all case expressions are executed once, including function calls in the case statement. It waits for all functions to return first. If there is a default branch, the default branch statement is executed. If there is no default branch, the SELECT statement blocks until at least one communication operation can take place. Therefore, the above program needs to wait for the return of case3 at 3000ms before entering the judgment of case that meets the condition. At this time, CasE1, Case2 and Case3 have all met the condition of readable. According to the rule that multiple cases can be read, a case will be randomly selected for read operation. The result is a random print of a value in 50, 200, 3000. To modify the above example, if all three cases call AsyncCall, then the program will print 50 after 50ms, because all three cases call AsyncCall methods immediately, and casE1 is ready first.

func AsyncCall(t int) <-chan int {
    c := make(chan int)
    go func(a) {
        time.Sleep(time.Millisecond * time.Duration(t))
        c <- t
    }()
    return c
}

func main(a) {
    select {
    case resp := <-AsyncCall(50):
        fmt.Println(resp)
    case resp := <-AsyncCall(200):
        fmt.Println(resp)
    case resp := <-AsyncCall(3000):
        fmt.Println(resp)
    }
}

// Print the result50Fixed printing after MS50
Copy the code

If there are multiple cases satisfying the execution condition in select and there is a default statement, the default statement is always satisfying the execution condition, then will there be a certain chance to execute the default statement at random? The answer is no, the default statement is executed only if all non-default statements do not satisfy the condition.

func AsyncCall(t int) <-chan int {
    c := make(chan int.1)
    go func(a) {
        time.Sleep(time.Millisecond * time.Duration(t))
        c <- t
    }()

    time.Sleep(100 * time.Millisecond)
    return c
}

func main(a) {

    select {
    case resp := <-AsyncCall(50):
        fmt.Println(resp)
    default:
        fmt.Println("default case run")}}// Print the result
50
Copy the code

In the select example above, both case1 and default meet the execution criteria, but the program always executes the non-default case first.

Select Case Indicates the priority

Select does not execute case statements sequentially, but if we need to implement a function to receive tasks from ch1 and CH2, if both ch1 and CH2 satisfy the execution conditions, We want to ensure that the task of CH1 is executed before that of CH2, that is, we need to implement a case execution order with priority. This can be achieved by defalut, for which we wrote the first version of the code as follows:

type Job func(a)

func main(a) {
    ch1 := make(chan Job, 10)
    ch2 := make(chan Job, 10)

    job1 := func(a) {
        fmt.Println("job1 run")
    }
    job2 := func(a) {
        fmt.Println("job2 run")
    }
    ch2 <- job2
    ch1 <- job1
    
    go func(a) {
        time.Sleep(100 * time.Second)
    }()

    PrioritySelect(ch1, ch2)
}

func PrioritySelect(ch1, ch2 chan Job) {
    for {
        select {
        case job1 := <-ch1:
            job1()
        default:
            select {
            case job2 := <-ch2:
                job2()
            }
        }
    }
}

// Result:
job1 run
job2 run
Copy the code

If the default case and the normal case meet the execution condition, the normal case will be executed first. If the normal case and the default case meet the execution condition, the normal case will be executed first. So we execute case1, which is job1, on the first for loop, and then on the second for loop case1 no longer meets the execution criteria and executes the statement in default, Default also calls a SELECT to listen for ch2 reads, so the second loop executes job2. In addition to the use of default, there is another way to implement it:

type Job func(a)

func main(a) {
    ch1 := make(chan Job, 10)
    ch2 := make(chan Job, 10)

    job1 := func(a) {
        fmt.Println("job1 run")
    }
    job2 := func(a) {
        fmt.Println("job2 run")
    }
    ch2 <- job2
    ch1 <- job1

    go func(a) {
        time.Sleep(100 * time.Second)
    }()

    PrioritySelect(ch1, ch2)
}

func PrioritySelect(ch1, ch2 chan Job) {
    for {
        select {
        case job1 := <-ch1:
            job1()
        case job2 := <-ch2:
        priority:
            for {
                select {
                case job1 := <-ch1:
                    job1()
                default:
                    break priority
                }
            }
            job2()
        }
    }
}
// Print the result:
job1 run
job2 run
Copy the code

If ch1 is executed randomly, then job1 will be executed first. If CH2 is executed randomly, then we will execute a for-select on CH1 in case2. If CH1 meets the condition, then we will execute a for-select on CH1. Job1 is executed, then joB2 is executed, and then the next loop is entered. Otherwise, if CH1 is not ready, the second layer for loop is exited and joB2 is executed and then the next loop of the first layer for loop is entered.

The for – select and timer. After

Time. After is used for timeout control. It is used to pass a timeout, return a time-blocking channel, and pass a value to the channel After waiting for the specified timeout. This allows the thread to listen for a channel read before continuing with the subsequent method. You can exit groutine with a timeout in combination with select, but using time.After can cause memory leaks. After(60* time.second), the timer will not be garbage collected until 60 seconds later.

// After waits for the duration to elapse and then sends the current time
// on the returned channel.
// It is equivalent to NewTimer(d).C.
// The underlying Timer is not recovered by the garbage collector
// until the timer fires. If efficiency is a concern, use NewTimer
// instead and call Timer.Stop if the timer is no longer needed.
func After(d Duration) <-chan Time {
    return NewTimer(d).C
}
Copy the code

The following example uses time.After to control the time out of the run function. The run function returns After 100ms, but the timer will not be released between 100ms and 3s. The timer is actually leaked.

type resp struct{}func run(a) <-chan resp {
    ch := make(chan resp)

    go func(a) {
        time.Sleep(100 * time.Millisecond)
        ch <- resp{}
    }()

    return ch
}

func main(a) {
    select {
    case resp := <-run():
        fmt.Println("run success", resp)
    case <-time.After(3 * time.Second):
        fmt.Println("timeout")
    }
    time.Sleep(100 * time.Second) // prevent main exit
}
Copy the code

If we use time.after in for-select, but other cases return soon, we will create too many timer objects and not collect them until the timeout period, which will consume a lot of memory and cause a memory leak.

type resp struct{}func run(a) <-chan resp {
    ch := make(chan resp)

    go func(a) {
        ch <- resp{}
    }()

    return ch
}

func main(a) {
    for {
        select {
        case resp := <-run(): // Return shortly
            fmt.Println("run success", resp)
        case <-time.After(3 * time.Second): // Memory leaks
            fmt.Println("timeout")}}}Copy the code

Since we cannot cancel timer.after voluntarily, we need a cancelable timer. Just as GO provides us with NewTimer and NewTicker, We can call timer.stop and ticker.Stop methods to actively cancel the timer or ticker.

type resp struct{}func run(a) <-chan resp {
    ch := make(chan resp)

    go func(a) {
        ch <- resp{}
    }()

    return ch
}

func main(a) {
    for {
        ticker := time.NewTimer(3 * time.Second)
        select {
        case resp := <-run():
            fmt.Println("run success", resp)
            ticker.Stop()
        case <-ticker.C:
            fmt.Println("timeout")}}}Copy the code