Go 的通道有兩種操作方式,一種是帶 range 子句的 for 語句,另一種則是 select 語句,它是專門為了操作通道而存在的。這裡主要介紹 select 的用法。
一、select的語法
select 語句的語法如下:
select {
case <-ch1 :
statement(s)
case ch2 <- 1 :
statement(s)
…
default : /* 可選 */
statement(s)
}
這裡要注意:
- 每個 case 都必須是一個通訊。
由於 select 語句是專為通道設計的,所以每個 case 表示式中都只能包含操作通道的表示式,比如接收表示式。 - 如果有多個 case 都可以執行,select 會隨機公平地選出一個執行,其他不會執行。
- 如果多個 case 都不能執行,若有 default 子句,則執行該語句,反之,select 將阻塞,直到某個 case 可以執行。
- 所有 channel 表示式都會被求值。
用一個簡單示例看一下:
package main
import (
"fmt"
"math/rand"
)
func main() {
// 準備好幾個通道。
intChannels := [5]chan int{
make(chan int, 1),
make(chan int, 1),
make(chan int, 1),
make(chan int, 1),
make(chan int, 1)
}
// 隨機選擇一個通道,並向它傳送元素值。
index := rand.Intn(5)
fmt.Printf("The index: %d\n", index)
intChannels[index] <- index
// 哪一個通道中有可取的元素值,哪個對應的分支就會被執行。
select {
case <-intChannels[0]:
fmt.Println("The first candidate case is selected.")
case <-intChannels[1]:
fmt.Println("The second candidate case is selected.")
case elem := <-intChannels[2]:
fmt.Printf("The third candidate case is selected. The element is %d.\n", elem)
default:
fmt.Println("No candidate case is selected!")
}
}
準備了5個通道,放到一個陣列裡,並用0-4的隨機數作為陣列的索引,向通道傳送元素。用一個包含了三個候選分支的 select 語句,分別嘗試從三個通道中接收元素值,哪一個通道中有值,哪一個對應的候選分支就會被執行。
執行結果如下:
The index: 1
The second candidate case is selected.
多次執行的話,會隨機輸出不同的字串,如果隨機值不是0、1、2,則會執行 default 語句。
二、select死鎖
select 使用不當會發生死鎖。
- 如果通道沒有資料傳送,但 select 中有存在接收通道資料的語句,將發生死鎖。
package main
func main() {
ch := make(chan string)
select {
case <-ch:
}
}
報錯如下:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
/workspace/src/test.go:5 +0x52
exit status 2
可以新增 default 語句來避免產生死鎖。
- 空 select{}
package main
func main() {
select {}
}
報錯如下:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [select (no cases)]:
main.main()
/workspace/src/test.go:3 +0x20
exit status 2
三、select和for結合使用
select 語句只能對其中的每一個case表示式各求值一次。所以,如果想連續或定時地操作其中的通道的話,就需要通過在for語句中嵌入select語句的方式實現。
package main
import (
"fmt"
"time"
)
func main() {
tick := time.Tick(time.Second)
for {
select {
case t := <-tick:
fmt.Println(t)
break
}
}
fmt.Println("end")
}
列印結果如下:
2021-10-10 23:40:59.664804 +0800 CST m=+1.001254136
2021-10-10 23:41:00.665263 +0800 CST m=+2.001696651
2021-10-10 23:41:01.665595 +0800 CST m=+3.002013571
2021-10-10 23:41:02.665293 +0800 CST m=+4.001699053
2021-10-10 23:41:03.665308 +0800 CST m=+5.001702570
2021-10-10 23:41:04.666859 +0800 CST m=+6.003244115
2021-10-10 23:41:05.665595 +0800 CST m=+7.001972958
……
你會發現 break 只跳出了 select,無法跳出for。
解決辦法有兩種:
- 使用 goto 跳出迴圈
package main
import (
"fmt"
"time"
)
func main() {
tick := time.Tick(time.Second)
for {
select {
case t := <-tick:
fmt.Println(t)
//跳到指定位置
goto END
}
}
END:
fmt.Println("end")
}
- 使用標籤
package main
import (
"fmt"
"time"
)
func main() {
tick := time.Tick(time.Second)
//這是標籤
FOREND:
for {
select {
case t := <-tick:
fmt.Println(t)
//跳出FOREND標籤
break ForEnd
}
}
END:
fmt.Println("end")
}
四、select實現超時機制
主要使用的 time.After
實現超時控制。
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
quit := make(chan bool)
go func() {
for {
select {
case num := <-ch: //如果有資料,下面列印。但是有可能ch一直沒資料
fmt.Println("num = ", num)
case <-time.After(3 * time.Second): //上面的ch如果一直沒資料會阻塞,那麼select也會檢測其他case條件,檢測到後3秒超時
fmt.Println("超時")
quit <- true //寫入
}
}
}()
for i := 0; i < 5; i++ {
ch <- i
time.Sleep(time.Second)
}
<-quit //這裡暫時阻塞,直到可讀
fmt.Println("程式結束")
}
執行後,可以觀察到:依次列印出0-4,幾秒過後列印出“超時”和“程式結束”,列印結果如下:
num = 0
num = 1
num = 2
num = 3
num = 4
超時
程式結束