這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。歡迎來到 [Golang 系列教程](https://studygolang.com/subject/2)的第 24 篇。 ## 什麼是 select?`select` 語句用於在多個發送/接收通道操作中進行選擇。`select` 語句會一直阻塞,直到發送/接收操作準備就緒。如果有多個通道操作準備完畢,`select` 會隨機地選取其中之一執行。該文法與 `switch` 類似,所不同的是,這裡的每個 `case` 語句都是通道操作。我們好好看一些代碼來加深理解吧。 ## 樣本```gopackage mainimport ( "fmt" "time")func server1(ch chan string) { time.Sleep(6 * time.Second) ch <- "from server1"}func server2(ch chan string) { time.Sleep(3 * time.Second) ch <- "from server2"}func main() { output1 := make(chan string) output2 := make(chan string) go server1(output1) go server2(output2) select { case s1 := <-output1: fmt.Println(s1) case s2 := <-output2: fmt.Println(s2) }}```[線上運行程式](https://play.golang.org/p/3_yaJSoSpG) 在上面程式裡,`server1` 函數(第 8 行)休眠了 6 秒,接著將文本 `from server1` 寫入通道 `ch`。而 `server2` 函數(第 12 行)休眠了 3 秒,然後把 `from server2` 寫入了通道 `ch`。 而 `main` 函數在第 20 行和第 21 行,分別調用了 `server1` 和 `server2` 兩個 Go 協程。 在第 22 行,程式運行到了 `select` 語句。`select` 會一直發生阻塞,除非其中有 case 準備就緒。在上述程式裡,`server1` 協程會在 6 秒之後寫入 `output1` 通道,而`server2` 協程在 3 秒之後就寫入了 `output2` 通道。因此 `select` 語句會阻塞 3 秒鐘,等著 `server2` 向 `output2` 通道寫入資料。3 秒鐘過後,程式會輸出: ```from server2```然後程式終止。 ## select 的應用在上面程式中,函數之所以取名為 `server1` 和 `server2`,是為了展示 `select` 的實際應用。 假設我們有一個關鍵性應用,需要儘快地把輸出返回給使用者。這個應用的資料庫複寫並且儲存在世界各地的伺服器上。假設函數 `server1` 和 `server2` 與這樣不同地區的兩台伺服器進行通訊。每台伺服器的負載和網路時延決定了它的回應時間。我們向兩台伺服器發送請求,並使用 `select` 語句等待相應的通道發出響應。`select` 會選擇首先響應的伺服器,而忽略其它的響應。使用這種方法,我們可以向多個伺服器發送請求,並給使用者返回最快的響應了。:) ## 預設情況在沒有 case 準備就緒時,可以執行 `select` 語句中的預設情況(Default Case)。這通常用於防止 `select` 語句一直阻塞。 ```gopackage mainimport ( "fmt" "time")func process(ch chan string) { time.Sleep(10500 * time.Millisecond) ch <- "process successful"}func main() { ch := make(chan string) go process(ch) for { time.Sleep(1000 * time.Millisecond) select { case v := <-ch: fmt.Println("received value: ", v) return default: fmt.Println("no value received") } }}```[線上運行程式](https://play.golang.org/p/8xS5r9g1Uy) 上述程式中,第 8 行的 `process` 函數休眠了 10500 毫秒(10.5 秒),接著把 `process successful` 寫入 `ch` 通道。在程式中的第 15 行,並發地調用了這個函數。 在並發地調用了 `process` 協程之後,主協程啟動了一個無限迴圈。這個無限迴圈在每一次迭代開始時,都會先休眠 1000 毫秒(1 秒),然後執行一個 select 操作。在最開始的 10500 毫秒中,由於 `process` 協程在 10500 毫秒後才會向 `ch` 通道寫入資料,因此 `select` 語句的第一個 case(即 `case v := <-ch:`)並未就緒。所以在這期間,程式會執行預設情況,該程式會列印 10 次 `no value received`。 在 10.5 秒之後,`process` 協程會在第 10 行向 `ch` 寫入 `process successful`。現在,就可以執行 `select` 語句的第一個 case 了,程式會列印 `received value: process successful`,然後程式終止。該程式會輸出: ```no value received no value received no value received no value received no value received no value received no value received no value received no value received no value received received value: process successful ```## 死結與預設情況```gopackage mainfunc main() { ch := make(chan string) select { case <-ch: }}```[線上運行程式](https://play.golang.org/p/za0GZ4o7HH) 上面的程式中,我們在第 4 行建立了一個通道 `ch`。我們在 `select` 內部(第 6 行),試圖讀取通道 `ch`。由於沒有 Go 協程向該通道寫入資料,因此 `select` 語句會一直阻塞,導致死結。該程式會觸發運行時 `panic`,報錯資訊如下: ```fatal error: all goroutines are asleep - deadlock!goroutine 1 [chan receive]: main.main() /tmp/sandbox416567824/main.go:6 +0x80```如果存在預設情況,就不會發生死結,因為在沒有其他 case 準備就緒時,會執行預設情況。我們用預設情況重寫後,程式如下: ```gopackage mainimport "fmt"func main() { ch := make(chan string) select { case <-ch: default: fmt.Println("default case executed") }}```[線上運行程式](https://play.golang.org/p/Pxsh_KlFUw) 以上程式會輸出: ```default case executed ```如果 `select` 只含有值為 `nil` 的通道,也同樣會執行預設情況。 ```gopackage mainimport "fmt"func main() { var ch chan string select { case v := <-ch: fmt.Println("received value", v) default: fmt.Println("default case executed") }}```[線上運行程式](https://play.golang.org/p/IKmGpN61m1) 在上面程式中,`ch` 等於 `nil`,而我們試圖在 `select` 中讀取 `ch`(第 8 行)。如果沒有預設情況,`select` 會一直阻塞,導致死結。由於我們在 `select` 內部加入了預設情況,程式會執行它,並輸出: ```default case executed ```## 隨機選取當 `select` 由多個 case 準備就緒時,將會隨機地選取其中之一去執行。 ```gopackage mainimport ( "fmt" "time")func server1(ch chan string) { ch <- "from server1"}func server2(ch chan string) { ch <- "from server2"}func main() { output1 := make(chan string) output2 := make(chan string) go server1(output1) go server2(output2) time.Sleep(1 * time.Second) select { case s1 := <-output1: fmt.Println(s1) case s2 := <-output2: fmt.Println(s2) }}```[線上運行程式](https://play.golang.org/p/vJ6VhVl9YY) 在上面程式裡,我們在第 18 行和第 19 行分別調用了 `server1` 和 `server2` 兩個 Go 協程。接下來,主程式休眠了 1 秒鐘(第 20 行)。當程式控制到達第 21 行的 `select` 語句時,`server1` 已經把 `from server1` 寫到了 `output1` 通道上,而 `server2` 也同樣把 `from server2` 寫到了 `output2` 通道上。因此這個 `select` 語句中的兩種情況都準備好執行了。如果你運行這個程式很多次的話,輸出會是 `from server1` 或者 `from server2`,這會根據隨機選取的結果而變化。 請在你的本地系統上運行這個程式,獲得程式的隨機結果。因為如果你在 playground 上線上啟動並執行話,它的輸出總是一樣的,這是由於 playground 不具有隨機性所造成的。 ## 這下我懂了:空 select```gopackage mainfunc main() { select {}}```[線上運行程式](https://play.golang.org/p/u8hErIxgxs) 你認為上面代碼會輸出什嗎? 我們已經知道,除非有 case 執行,select 語句就會一直阻塞著。在這裡,`select` 語句沒有任何 case,因此它會一直阻塞,導致死結。該程式會觸發 panic,輸出如下: ```fatal error: all goroutines are asleep - deadlock!goroutine 1 [select (no cases)]: main.main() /tmp/sandbox299546399/main.go:4 +0x20```本教程到此結束。祝你愉快。 **上一教程 - [緩衝通道和工作池](https://studygolang.com/articles/12512)****下一教程 - [Mutex](https://studygolang.com/articles/12598)**
via: https://golangbot.com/select/
作者:Nick Coghlan 譯者:Noluye 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
2588 次點擊