Go的Select

鹿呦呦發表於2021-10-10

Go 的通道有兩種操作方式,一種是帶 range 子句的 for 語句,另一種則是 select 語句,它是專門為了操作通道而存在的。這裡主要介紹 select 的用法。

一、select的語法

select 語句的語法如下:

select {
    case <-ch1 :
       statement(s)   
    case ch2 <- 1 :
       statement(s)
    …
    default : /* 可選 */
       statement(s)
}

這裡要注意:

  • 每個 case 都必須是一個通訊。
    由於 select 語句是專為通道設計的,所以每個 case 表示式中都只能包含操作通道的表示式,比如接收表示式。
  • 如果有多個 case 都可以執行,select 會隨機公平地選出一個執行,其他不會執行。
  • 如果多個 case 都不能執行,若有 default 子句,則執行該語句,反之,select 將阻塞,直到某個 case 可以執行。
  • 所有 channel 表示式都會被求值。

用一個簡單示例看一下:

package main

import (
    "fmt"
    "math/rand"
)

func main() {
    // 準備好幾個通道。
    intChannels := [5]chan int{
        make(chan int, 1),
        make(chan int, 1),
        make(chan int, 1),
        make(chan int, 1),
        make(chan int, 1)
    }
    // 隨機選擇一個通道,並向它傳送元素值。
    index := rand.Intn(5)
    fmt.Printf("The index: %d\n", index)
    intChannels[index] <- index
    // 哪一個通道中有可取的元素值,哪個對應的分支就會被執行。
    select {
        case <-intChannels[0]:
            fmt.Println("The first candidate case is selected.")
        case <-intChannels[1]:
            fmt.Println("The second candidate case is selected.")
        case elem := <-intChannels[2]:
            fmt.Printf("The third candidate case is selected. The element is %d.\n", elem)
        default:
            fmt.Println("No candidate case is selected!")
    }
}

準備了5個通道,放到一個陣列裡,並用0-4的隨機數作為陣列的索引,向通道傳送元素。用一個包含了三個候選分支的 select 語句,分別嘗試從三個通道中接收元素值,哪一個通道中有值,哪一個對應的候選分支就會被執行。
執行結果如下:

The index: 1
The second candidate case is selected.

多次執行的話,會隨機輸出不同的字串,如果隨機值不是0、1、2,則會執行 default 語句。

二、select死鎖

select 使用不當會發生死鎖。

  • 如果通道沒有資料傳送,但 select 中有存在接收通道資料的語句,將發生死鎖。
package main
    func main() {  
        ch := make(chan string)
        select {
            case <-ch:
        }
    }

報錯如下:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
	/workspace/src/test.go:5 +0x52
exit status 2

可以新增 default 語句來避免產生死鎖。

  • 空 select{}
package main
    func main() {  
        select {}
    }

報錯如下:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [select (no cases)]:
main.main()
	/workspace/src/test.go:3 +0x20
exit status 2
三、select和for結合使用

select 語句只能對其中的每一個case表示式各求值一次。所以,如果想連續或定時地操作其中的通道的話,就需要通過在for語句中嵌入select語句的方式實現。

package main

import (
    "fmt"
    "time"
)

func main() {
    tick := time.Tick(time.Second)
    for {
        select {
            case t := <-tick:
                fmt.Println(t)
                break
            }
        }
        fmt.Println("end")
    }

列印結果如下:

2021-10-10 23:40:59.664804 +0800 CST m=+1.001254136
2021-10-10 23:41:00.665263 +0800 CST m=+2.001696651
2021-10-10 23:41:01.665595 +0800 CST m=+3.002013571
2021-10-10 23:41:02.665293 +0800 CST m=+4.001699053
2021-10-10 23:41:03.665308 +0800 CST m=+5.001702570
2021-10-10 23:41:04.666859 +0800 CST m=+6.003244115
2021-10-10 23:41:05.665595 +0800 CST m=+7.001972958
……

你會發現 break 只跳出了 select,無法跳出for。
解決辦法有兩種:

  • 使用 goto 跳出迴圈
package main

import (
    "fmt"
    "time"
)

func main() {
    tick := time.Tick(time.Second)
    for {
        select {
            case t := <-tick:
                fmt.Println(t)
                //跳到指定位置
                goto END
            }
        }
END:
        fmt.Println("end")
    }
  • 使用標籤
package main

import (
    "fmt"
    "time"
)

func main() {
    tick := time.Tick(time.Second)
//這是標籤
FOREND:
    for {
        select {
            case t := <-tick:
                fmt.Println(t)
                //跳出FOREND標籤
                break ForEnd
            }
        }
END:
        fmt.Println("end")
    }
四、select實現超時機制

主要使用的 time.After 實現超時控制。

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan int)
    quit := make(chan bool)

    go func() {
        for {
            select {
            case num := <-ch:  //如果有資料,下面列印。但是有可能ch一直沒資料
                fmt.Println("num = ", num)
            case <-time.After(3 * time.Second): //上面的ch如果一直沒資料會阻塞,那麼select也會檢測其他case條件,檢測到後3秒超時
                fmt.Println("超時")
                quit <- true  //寫入
            }
        }

    }()

    for i := 0; i < 5; i++ {
        ch <- i
        time.Sleep(time.Second)
    }
    <-quit //這裡暫時阻塞,直到可讀
    fmt.Println("程式結束")
}

執行後,可以觀察到:依次列印出0-4,幾秒過後列印出“超時”和“程式結束”,列印結果如下:

num =  0
num =  1
num =  2
num =  3
num =  4
超時
程式結束

相關文章