Go 語言內(nèi)置 I/O 多路復(fù)用機(jī)制
01 介紹
Go 協(xié)程之間通過 channel 通信,但是 channel 讀寫取決于自身特性,即是否有可寫入緩沖區(qū)、緩沖區(qū)中是否有數(shù)據(jù)、是否已關(guān)閉...
為了檢測(cè) channel 的特性,Go 提供了一個(gè)關(guān)鍵字 select,可用于實(shí)現(xiàn) I/O 多路復(fù)用機(jī)制。
本文我們介紹 Go 關(guān)鍵字 select 的使用方式。
02 使用方式
Go 關(guān)鍵字 select 中包含 case 語句和 default 語句,其中 default 語句可以認(rèn)為是一種特殊的 case 語句。
因?yàn)?nbsp;default 語句不負(fù)責(zé)處理 channel 的讀寫,它可以在 select 中的任意位置,且僅能包含一個(gè) default 語句。在所有 case 語句都不滿足執(zhí)行條件時(shí),default 語句將被執(zhí)行(建議盡量不要省略 default 語句)。
我們通過代碼片段,分別介紹 select 在檢測(cè)到 channel 不同特性時(shí),得到的運(yùn)行結(jié)果。
空 select
接下來,我們閱讀一段代碼。
func main() {
fmt.Println("Golang 語言開發(fā)棧")
go func() {
fmt.Println("Golang 公眾號(hào)")
}()
}
閱讀上面這段代碼,讀者朋友們認(rèn)為 Go 協(xié)程中的打印語句可以正常輸出嗎?
讀者朋友們?nèi)绻\(yùn)行代碼,會(huì)發(fā)現(xiàn) Go 協(xié)程中的打印語句還沒有執(zhí)行,程序就已經(jīng)退出了,這是因?yàn)?nbsp;main 函數(shù)中的打印語句已經(jīng)執(zhí)行完成,所以會(huì)退出程序。
如果我們希望 Go 協(xié)程中的打印語句也執(zhí)行,可以在 main 函數(shù)中使用 select{} 將 main 阻塞,Go 協(xié)程中的打印語句就有機(jī)會(huì)執(zhí)行了。但是,這會(huì)導(dǎo)致死鎖(可以根據(jù)實(shí)際應(yīng)用場景選擇是否使用)。
無緩沖 channel
接下來,我們?cè)僮x一段可以導(dǎo)致死鎖的代碼:
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)
}
}
閱讀上面這段代碼,我們定義一個(gè)函數(shù) DoChannel(),該函數(shù)接收的參數(shù)是一個(gè) string 類型的 channel,函數(shù)體中使用 select 中的兩個(gè) case 語句,分別對(duì)參數(shù)進(jìn)行接收和發(fā)送操作。
運(yùn)行代碼,select 阻塞。
因?yàn)?,我們傳參?nbsp;c 是無緩沖 channel,所以它即不能讀也不能寫,兩個(gè) case 語句都不執(zhí)行,select 陷入阻塞,導(dǎo)致死鎖(此處為了行文,故意沒有 default 語句)。
無數(shù)據(jù),有緩沖channel
我們將上面這段代碼,稍微修改一下,將入?yún)⒌?nbsp;c 改為 1 個(gè)緩沖區(qū)大小的 channel(未寫入數(shù)據(jù))。代碼如下:
func main() {
c := make(chan string, 1)
DoChannel(c)
}
運(yùn)行代碼,寫執(zhí)行,讀未執(zhí)行。
即 select 中的對(duì)入?yún)?nbsp;channel 進(jìn)行發(fā)送操作的 case 語句被執(zhí)行,因?yàn)槿雲(yún)?nbsp;c 是一個(gè)有 1 個(gè)緩沖區(qū)大小的 channel,并且該 channel 中還沒有數(shù)據(jù),所以讀取操作的 case 語句沒有讀取到數(shù)據(jù),不滿足執(zhí)行條件。
有緩沖區(qū),已寫滿數(shù)據(jù) channel
我們?cè)傩薷囊幌氯雲(yún)?nbsp;c,將入?yún)⒌?nbsp;c 改為 1 個(gè)緩沖區(qū)大小的 channel,并且寫入字符串 Go。代碼如下:
func main() {
c := make(chan string, 1)
c <- "Go"
DoChannel(c)
}
運(yùn)行代碼,讀執(zhí)行,寫未執(zhí)行。
即 select 中的對(duì)入?yún)?nbsp;channel 進(jìn)行接收操作的 case 語句被執(zhí)行,因?yàn)槿雲(yún)?nbsp;c 是一個(gè)有 1 個(gè)緩沖區(qū)大小,并且已寫滿數(shù)據(jù),所以讀取操作的 case 語句可以讀取到數(shù)據(jù),滿足執(zhí)行條件。
而寫入操作的 case 無法寫入數(shù)據(jù),不滿足執(zhí)行條件。
有緩沖區(qū),有數(shù)據(jù),可寫數(shù)據(jù) channel
最后一種場景是既能讀取也能寫入的 channel,我們修改一下入?yún)?nbsp;c,將入?yún)?nbsp;c 改為 2 個(gè)緩沖區(qū)大小的 channel,其中 1 個(gè)緩沖區(qū)寫入字符串 Go,另外 1 個(gè)緩沖區(qū)還可以寫入數(shù)據(jù)。代碼如下:
func main() {
c := make(chan string, 2)
c <- "Go"
DoChannel(c)
}
通過多次運(yùn)行代碼,會(huì)發(fā)現(xiàn)讀取和寫入的 case 語句都有機(jī)會(huì)執(zhí)行,因?yàn)閮蓚€(gè) case 語句都滿足執(zhí)行條件,但是只能有 1 個(gè) case 語句執(zhí)行,select 會(huì)隨機(jī)執(zhí)行其中 1 個(gè) case 語句。
至此,我們已經(jīng)介紹了 5 種 channel 在 select 中的運(yùn)行結(jié)果。
case 語句中聲明變量
上面的代碼中,我們發(fā)現(xiàn)在兩個(gè) case 語句中,讀操作我們將讀取到的數(shù)據(jù)賦值給變量 receive,實(shí)際上,我們也可以省略變量賦值操作。
如果我們需要將讀取到的數(shù)據(jù),賦值給變量的話,一般建議將讀取 channel 返回的兩個(gè)值全部接收,其中一個(gè)是讀取到的數(shù)據(jù),另外一個(gè)是布爾值,代表 channel 中沒有數(shù)據(jù),并且已被關(guān)閉。代碼如下:
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)
}
}
}
閱讀上面這段代碼,我們使用 close 將 c 關(guān)閉。select 中的讀操作 case 語句,可以通過 ok 的值,得到 channel 中沒有數(shù)據(jù),且已被關(guān)閉,不必打印空數(shù)據(jù)。
03 總結(jié)
本文我們了解到 select 中的 case 語句可以讀取 channel,多個(gè) case 語句僅能其中 1 個(gè)被執(zhí)行。
每個(gè) case 語句僅能對(duì) 1 個(gè) channel 進(jìn)行讀寫操作,如果讀操作未讀取到數(shù)據(jù)將陷入阻塞,如果寫操作無法寫入數(shù)據(jù)將陷入阻塞,如果所有 case 語句中的 channel 都陷入阻塞時(shí),select 也會(huì)陷入阻塞。
為了避免 select 陷入阻塞,我們可以使用 default 語句,需要注意的是,default 語句可以在 select 的任意位置,但是僅能包含 1 個(gè),而 case 語句可以包含多個(gè)。