歡迎來到 Golang 系列教程[1]的第 24 篇。
什麼是 select?select 語句用於在多個發送/接收信道操作中進行選擇。select 語句會一直阻塞,直到發送/接收操作準備就緒。如果有多個信道操作準備完畢,select 會隨機地選取其中之一執行。該語法與 switch 類似,所不同的是,這裡的每個 case 語句都是信道操作。我們好好看一些代碼來加深理解吧。
示例package main
import (
"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)
}
}在線運行程序[2]
在上面程序裡,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 語句一直阻塞。
package main
import (
"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")
}
}
}在線運行程序[3]
上述程序中,第 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
死鎖與默認情況package main
func main() {
ch := make(chan string)
select {
case <-ch:
}
}在線運行程序[4]
上面的程序中,我們在第 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 準備就緒時,會執行默認情況。我們用默認情況重寫後,程序如下:
package main
import "fmt"
func main() {
ch := make(chan string)
select {
case <-ch:
default:
fmt.Println("default case executed")
}
}在線運行程序[5]
以上程序會輸出:
default case executed如果 select 只含有值為 nil 的信道,也同樣會執行默認情況。
package main
import "fmt"
func main() {
var ch chan string
select {
case v := <-ch:
fmt.Println("received value", v)
default:
fmt.Println("default case executed")
}
}在線運行程序[6]
在上面程序中,ch 等於 nil,而我們試圖在 select 中讀取 ch(第 8 行)。如果沒有默認情況,select 會一直阻塞,導致死鎖。由於我們在 select 內部加入了默認情況,程序會執行它,並輸出:
default case executed
隨機選取當 select 由多個 case 準備就緒時,將會隨機地選取其中之一去執行。
package main
import (
"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)
}
}在線運行程序[7]
在上面程序裡,我們在第 18 行和第 19 行分別調用了 server1 和 server2 兩個 Go 協程。接下來,主程序休眠了 1 秒鐘(第 20 行)。當程序控制到達第 21 行的 select 語句時,server1 已經把 from server1 寫到了 output1 信道上,而 server2 也同樣把 from server2 寫到了 output2 信道上。因此這個 select 語句中的兩種情況都準備好執行了。如果你運行這個程序很多次的話,輸出會是 from server1 或者 from server2,這會根據隨機選取的結果而變化。
請在你的本地系統上運行這個程序,獲得程序的隨機結果。因為如果你在 playground 上在線運行的話,它的輸出總是一樣的,這是由於 playground 不具有隨機性所造成的。
這下我懂了:空 selectpackage main
func main() {
select {}
}在線運行程序[8]
你認為上面代碼會輸出什麼?
我們已經知道,除非有 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本教程到此結束。祝你愉快。
上一教程 - 緩衝信道和工作池
下一教程 - Mutex[9]
via: https://golangbot.com/select/
作者:Nick Coghlan[10]譯者:Noluye[11]校對:polaris1119[12]
本文由 GCTT[13] 原創編譯,Go 中文網[14] 榮譽推出
參考資料[1]Golang 系列教程: https://studygolang.com/subject/2
[2]在線運行程序: https://play.golang.org/p/3_yaJSoSpG
[3]在線運行程序: https://play.golang.org/p/8xS5r9g1Uy
[4]在線運行程序: https://play.golang.org/p/za0GZ4o7HH
[5]在線運行程序: https://play.golang.org/p/Pxsh_KlFUw
[6]在線運行程序: https://play.golang.org/p/IKmGpN61m1
[7]在線運行程序: https://play.golang.org/p/vJ6VhVl9YY
[8]在線運行程序: https://play.golang.org/p/u8hErIxgxs
[9]Mutex: https://studygolang.com/articles/12598
[10]Nick Coghlan: https://golangbot.com/about/
[11]Noluye: https://github.com/Noluye
[12]polaris1119: https://github.com/polaris1119
[13]GCTT: https://github.com/studygolang/GCTT
[14]Go 中文網: https://studygolang.com/