Go 知识点(09)— for select 作用于 channel
1. for select 作用于未關閉的通道
1.1 沒有 default 分之場景
先看下面代碼
func main() {ch := make(chan int, 3)go func() {time.Sleep(2 * time.Second) // 延遲往通道里里面發送數據ch <- 1}()for {select {case v, ok := <-ch:fmt.Printf("v=%v, ok=%v\n", v, ok)time.Sleep(1 * time.Second)}fmt.Println("waiting")}
}
執行代碼輸出結果如下:
v=1, ok=true
waiting
fatal error: all goroutines are asleep - deadlock!goroutine 1 [chan receive]:
main.main()/home/wohu/project/go/src/demo/demo.go:17 +0x82
exit status 2
從結果我們可以看到:
- 當通道中沒有數據時,且
select語句沒有default分之時,會一直阻塞在case語句中; - 當通道中有數據時,
case語句拿到通道里面的值之后,繼續執行select語句塊之外的其余for循環體語句; - 當通道里面的數據被取走之后,
case語句一直等待從通道中取數據,但是一直沒有數據發送過來就會造成死鎖;
怎么避免這個死鎖問題呢? 這就需要加上 default 分之了。
1.2 有 default 分之場景
查看下面代碼
func main() {ch := make(chan int, 3)go func() {time.Sleep(2 * time.Second) // 延遲往通道里里面發送數據ch <- 1}()for {select {case v, ok := <-ch:fmt.Printf("v=%v, ok=%v\n", v, ok)time.Sleep(1 * time.Second)default:fmt.Println("通道沒有數據")time.Sleep(1 * time.Second)}fmt.Println("waiting")}
}
運行代碼輸出結果如下:
通道沒有數據
waiting
通道沒有數據
waiting
v=1, ok=true
waiting
通道沒有數據
waiting
通道沒有數據
從結果我們可以看到:
- 當通道中沒有數據時,走
default分之,default分之完成后會繼續執行for循環體的其它語句; - 當通道中有數據,則會執行對應的
case分之; - 當通道再一次沒有數據時,則繼續會執行
default分之和剩余的其它for循環體語句,而且會一直死循環執行;
2. for select 作用于關閉的通道
2.1 對關閉的通道執行 case 會造成死循環
繼續下面代碼
func main() {ch := make(chan int, 3)go func() {time.Sleep(2 * time.Second) // 延遲往通道里里面發送數據ch <- 1close(ch)}()for {select {case v, ok := <-ch:fmt.Printf("v=%v, ok=%v\n", v, ok)time.Sleep(1 * time.Second)default:fmt.Println("通道沒有數據")time.Sleep(1 * time.Second)}fmt.Println("waiting")}
}
運行輸出結果
通道沒有數據
waiting
通道沒有數據
waiting
v=1, ok=true
waiting
v=0, ok=false
waiting
v=0, ok=false
...
注意我們在前面已經將通道關閉,這個時候的 case 語句依然成立,所以會形成死循環執行這個 case 語句。
那么怎樣能跳出這個死循環的 case 語句呢?
2.2 跳出死循環的 case 語句
要跳出這個死循環的 case 語句,我們需要在 case 中通過第二個參數判斷 chan 是否關閉,如果關閉則通過 make(chan type) 來將關閉的 chan 置 nil ,當再次執行到 select 時,因為 chan 是 nil 會進入阻塞。
而 select 中如果任意某個分之可讀(包括 default ),它就會被執行,其他被忽略。所以在有 default 分之場景時, select 會跳過這個阻塞 case ,去執行 default 分之,這樣就可以避開這個死循環的 case 分之。
func main() {ch := make(chan int, 3)go func() {time.Sleep(2 * time.Second) // 延遲往通道里里面發送數據ch <- 1close(ch)}()for {select {case v, ok := <-ch:if !ok {ch = make(chan int)fmt.Println("通道已經關閉")} else {fmt.Printf("v=%v, ok=%v\n", v, ok)time.Sleep(1 * time.Second)}default:fmt.Println("通道沒有數據")time.Sleep(1 * time.Second)}fmt.Println("waiting")}
}
輸出結果如下:
通道沒有數據
waiting
通道沒有數據
waiting
v=1, ok=true
waiting
通道已經關閉
waiting
通道沒有數據
waiting
...
會一直循環打印 default 分之的輸出,那怎樣跳出這個循環呢?
2.3 跳出 for select 循環語句
- 可以使用
goto加lable跳轉到for外面; - 可以設置一個額外的標記位,當
chan關閉時,設置flag=true,在for的最后判斷flag決定是否break;
我們采用第二種方案:
func main() {ch := make(chan int, 3)go func() {time.Sleep(2 * time.Second) // 延遲往通道里里面發送數據ch <- 1close(ch)}()exitFlag := falsefor {select {case v, ok := <-ch:if !ok {ch = make(chan int)fmt.Println("通道已經關閉")exitFlag = true} else {fmt.Printf("v=%v, ok=%v\n", v, ok)time.Sleep(1 * time.Second)}default:fmt.Println("通道沒有數據")time.Sleep(1 * time.Second)}if exitFlag {fmt.Println("跳出循環")break}fmt.Println("waiting")}
}
輸出結果
通道沒有數據
waiting
通道沒有數據
waiting
v=1, ok=true
waiting
通道已經關閉
跳出循環
由以上示例我們可以得出以下結論:
select語句中如果任意某個case的通道有值可讀時,它就會被執行,其他case會被忽略;- 如果沒有
default語句,select將有可能阻塞,直到某個case分之有值可以運行,所以select里最好有一個default,否則將有一直阻塞的風險;
如果 select 語句發現同時有多個候選分支滿足選擇條件,那么它就會用一種偽隨機的算法在這些分支中選擇一個并執行。
僅當 select 語句中的所有 case 表達式都被求值完畢后,它才會開始選擇候選分支。這時候,它只會挑選滿足選擇條件的候選分支執行。如果所有的候選分支都不滿足選擇條件,那么默認分支就會被執行。如果這時沒有默認分支,那么 select 語句就會立即進入阻塞狀態,直到至少有一個候選分支滿足選擇條件為止。一旦有一個候選分支滿足選擇條件,select 語句就會被喚醒,這個候選分支就會被執行。
總結
以上是生活随笔為你收集整理的Go 知识点(09)— for select 作用于 channel的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2022-2028年中国激光全息膜行业市
- 下一篇: 2022-2028年中国氢化环氧树脂产业