Go 語言內置 I-O 多路複用機制

大家好,我是 frank。
歡迎大家關注「Golang 語言開發棧」公衆號。

01 介紹

Go 協程之間通過 channel 通信,但是 channel 讀寫取決於自身特性,即是否有可寫入緩衝區、緩衝區中是否有數據、是否已關閉...

爲了檢測 channel 的特性,Go 提供了一個關鍵字 select,可用於實現 I/O 多路複用機制。

本文我們介紹 Go 關鍵字 select 的使用方式。

02 使用方式

Go 關鍵字 select 中包含 case 語句和 default 語句,其中 default 語句可以認爲是一種特殊的 case 語句。

因爲 default 語句不負責處理 channel 的讀寫,它可以在 select 中的任意位置,且僅能包含一個 default 語句。在所有 case 語句都不滿足執行條件時,default 語句將被執行(建議儘量不要省略 default 語句)。

我們通過代碼片段,分別介紹 select 在檢測到 channel 不同特性時,得到的運行結果。

select

接下來,我們閱讀一段代碼。

func main() {
 fmt.Println("Golang 語言開發棧")
 go func() {
  fmt.Println("Golang 公衆號")
 }()
}

閱讀上面這段代碼,讀者朋友們認爲 Go 協程中的打印語句可以正常輸出嗎?

讀者朋友們如果運行代碼,會發現 Go 協程中的打印語句還沒有執行,程序就已經退出了,這是因爲 main 函數中的打印語句已經執行完成,所以會退出程序。

如果我們希望 Go 協程中的打印語句也執行,可以在 main 函數中使用 select{}main 阻塞,Go 協程中的打印語句就有機會執行了。但是,這會導致死鎖(可以根據實際應用場景選擇是否使用)。

無緩衝 channel

接下來,我們再讀一段可以導致死鎖的代碼:

func main() {
 c := make(chan string)
 DoChannel(c)
}

func DoChannel(c chan string) {
 var receive string
 send := "golang"
 select {
 case receive = <-c:
  fmt.Println(receive)
 case c <- send:
  fmt.Println(send)
 }
}

閱讀上面這段代碼,我們定義一個函數 DoChannel(),該函數接收的參數是一個 string 類型的 channel,函數體中使用 select 中的兩個 case 語句,分別對參數進行接收和發送操作。

運行代碼,select 阻塞。

因爲,我們傳參的 c 是無緩衝 channel,所以它即不能讀也不能寫,兩個 case 語句都不執行,select 陷入阻塞,導致死鎖(此處爲了行文,故意沒有 default 語句)。

無數據,有緩衝channel

我們將上面這段代碼,稍微修改一下,將入參的 c 改爲 1 個緩衝區大小的 channel(未寫入數據)。代碼如下:

func main() {
 c := make(chan string, 1)
 DoChannel(c)
}

運行代碼,寫執行,讀未執行。

select 中的對入參 channel 進行發送操作的 case 語句被執行,因爲入參 c 是一個有 1 個緩衝區大小的 channel,並且該 channel 中還沒有數據,所以讀取操作的 case 語句沒有讀取到數據,不滿足執行條件。

有緩衝區,已寫滿數據 channel

我們再修改一下入參 c,將入參的 c 改爲 1 個緩衝區大小的 channel,並且寫入字符串 Go。代碼如下:

func main() {
 c := make(chan string, 1)
 c <- "Go"
 DoChannel(c)
}

運行代碼,讀執行,寫未執行。

select 中的對入參 channel 進行接收操作的 case 語句被執行,因爲入參 c 是一個有 1 個緩衝區大小,並且已寫滿數據,所以讀取操作的 case 語句可以讀取到數據,滿足執行條件。

而寫入操作的 case 無法寫入數據,不滿足執行條件。

有緩衝區,有數據,可寫數據 channel

最後一種場景是既能讀取也能寫入的 channel,我們修改一下入參 c,將入參 c 改爲 2 個緩衝區大小的 channel,其中 1 個緩衝區寫入字符串 Go,另外 1 個緩衝區還可以寫入數據。代碼如下:

func main() {
 c := make(chan string, 2)
 c <- "Go"
 DoChannel(c)
}

通過多次運行代碼,會發現讀取和寫入的 case 語句都有機會執行,因爲兩個 case 語句都滿足執行條件,但是隻能有 1 個 case 語句執行,select 會隨機執行其中 1 個 case 語句。

至此,我們已經介紹了 5 種 channelselect 中的運行結果。

case 語句中聲明變量

上面的代碼中,我們發現在兩個 case 語句中,讀操作我們將讀取到的數據賦值給變量 receive,實際上,我們也可以省略變量賦值操作。

如果我們需要將讀取到的數據,賦值給變量的話,一般建議將讀取 channel 返回的兩個值全部接收,其中一個是讀取到的數據,另外一個是布爾值,代表 channel 中沒有數據,並且已被關閉。代碼如下:

func main() {
 c := make(chan string)
 close(c)
 DoChannelV2(c)
}

func DoChannelV2(c chan string) {
 var (
  receive string
  ok      bool
 )
 select {
 case receive, ok = <-c:
  if !ok {
   fmt.Println("no data")
  } else {
   fmt.Println(receive)
  }
 }
}

閱讀上面這段代碼,我們使用 closec 關閉。select 中的讀操作 case 語句,可以通過 ok 的值,得到 channel 中沒有數據,且已被關閉,不必打印空數據。

03 總結

本文我們瞭解到 select 中的 case 語句可以讀取 channel,多個 case 語句僅能其中 1 個被執行。

每個 case 語句僅能對 1 個 channel 進行讀寫操作,如果讀操作未讀取到數據將陷入阻塞,如果寫操作無法寫入數據將陷入阻塞,如果所有 case 語句中的 channel 都陷入阻塞時,select 也會陷入阻塞。

爲了避免 select 陷入阻塞,我們可以使用 default 語句,需要注意的是,default 語句可以在 select 的任意位置,但是僅能包含 1 個,而 case 語句可以包含多個。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/80V-IApqbAfOBvS3QoLb3A