golang教程之Select
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)
}
}
在上面的程式中,server1
函式中睡眠6秒然後將文字從server1
寫入通道ch。 server2
的功能是睡眠3秒,然後從server2
寫入通道ch。
main函式呼叫go no Gorosines server1
和server2
。
第22行中控制器到達select
語句。 select
語句將阻塞,直到其中一個case
準備就緒。 在上面的程式中,server1
Goroutine在6秒後寫入output1
通道,而server2
在3秒後寫入output2
通道。 因此select
語句將阻塞3秒並等待server2
output2
通道。 3秒後,程式打印出來,
from server2
然後將終止。
實際使用Select
將上述程式中的函式命名為server1
和server2
的原因是為了說明select的實際用途。
讓我們假設我們有一個關鍵任務應用程式,我們需要儘快將輸出返回給使用者。該應用程式的資料庫被複制並存儲在世界各地的不同伺服器中。假設函式server1
和server2
實際上與2個這樣的伺服器通訊。每個伺服器的響應時間取決於每個伺服器的負載和網路延遲。我們將請求傳送到兩個伺服器,然後使用select
語句在相應的通道上等待響應。首先響應的伺服器由select
選擇,其他響應被忽略。這樣我們就可以向多個伺服器傳送相同的請求,並將最快的響應返回給使用者。
預設case
當其他case
都沒有準備就緒時,將執行select
語句中的預設情況。這通常用於防止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")
}
}
}
在上面的程式中,process
函式睡眠10500毫秒(10.5秒),然後將處理成功寫入ch通道。此函式在第15行中同時呼叫。
在同時呼叫process
Goroutine之後,在主Goroutine中啟動了無限迴圈。無限迴圈在每次迭代開始期間休眠1000毫秒(1秒),並執行select
操作。在前10500毫秒期間,select
語句的第一種情況即case v:= <-ch:
將不會準備就緒,因為process
Goroutine將僅在10500毫秒之後寫入ch通道。因此,在此期間將執行預設情況,程式將不會列印10次接收的值。
在10.5秒之後,process
Goroutine將process
成功寫入ch。 現在將執行select
語句的第一種情況,程式將列印接收的值: 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
死鎖和預設case
package main
func main() {
ch := make(chan string)
select {
case <-ch:
}
}
在上面的程式中,我們在第4行建立了一個通道ch。 我們試著在第6行中選擇這個通道。 select
語句將永遠阻塞,因為沒有其他Goroutine寫入此通道,因此將導致死鎖。 該程式將在執行時出現以下訊息而發生混亂,
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
/tmp/sandbox416567824/main.go:6 +0x80
如果存在預設情況,則不會發生此死鎖,因為在沒有其他情況準備就緒時將執行預設情況。 上面的程式用下面的預設情況重寫。
package main
import "fmt"
func main() {
ch := make(chan string)
select {
case <-ch:
default:
fmt.Println("default case executed")
}
}
以上程式將列印,
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")
}
}
在上面的程式中,ch是nil,我們試圖從select中讀取ch。如果預設情況不存在,則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)
}
}
在上面的程式中,server1
和server2
go例程在第18和19行中呼叫。然後主程式在第20行中休眠1秒。 當控制器到達第21行中的select
語句時。server1
將從server1
寫入output1
通道,server2
將從server2
寫入output2
通道,因此select
語句的兩種情況都準備好執行。 如果多次執行此程式,輸出將在server1
或server2
之間變化,具體取決於隨機選擇的情況。
請在本地系統中執行此程式以獲得此隨機性。 如果該程式在playground上執行,它將列印相同的輸出,因為playground是確定性的。
常見問題 - 空選擇
package main
func main() {
select {}
}
您認為上述程式的輸出結果如何?
我們知道select
語句將被阻塞,直到其中一個case被執行。 在這種情況下,select語句沒有任何情況,因此它將永遠阻塞,從而導致死鎖。 這個程式會因以下輸出而感到恐慌,
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [select (no cases)]:
main.main()
/tmp/sandbox299546399/main.go:4 +0x20