29. Go 語言中的 select 用法

Hi,大家好,我是明哥。

在自己學習 Golang 的這段時間裏,我寫了詳細的學習筆記放在我的個人微信公衆號 《Go編程時光》,對於 Go 語言,我也算是個初學者,因此寫的東西應該會比較適合剛接觸的同學,如果你也是剛學習 Go 語言,不防關注一下,一起學習,一起成長。

我的在線博客:http://golang.iswbm.com
我的 Github:github.com/iswbm/GolangCodingTime


前面寫過兩節關於 switch-case 的文章,分別是:

流程控制:switch-case

Go 語言中的類型斷言

今天要學習一個跟 switch-case 很像,但還有點個人特色select-case,這一節本應該放在 學習 Go 協程:詳解信道/通道 裏一起講的,但是當時漏了,直到有讀者給我提出,才注意到,今天就用這篇文章補充一下。

跟 switch-case 相比,select-case 用法比較單一,它僅能用於 信道/通道 的相關操作。

select {
	case 表達式1:
		<code>
	case 表達式2:
		<code>
  default:
  	<code>
}

接下來,我們來看幾個例子幫助理解這個 select 的模型。

1. 最簡單的例子

先創建兩個信道,並在 select 前往 c2 發送數據

package main

import (
	"fmt"
)

func main() {
	c1 := make(chan string, 1)
	c2 := make(chan string, 1)

	c2 <- "hello"

	select {
    case msg1 := <-c1:
      fmt.Println("c1 received: ", msg1)
    case msg2 := <-c2:
      fmt.Println("c2 received: ", msg2)
    default:
      fmt.Println("No data received.")
    }
}

在運行 select 時,會遍歷所有(如果有機會的話)的 case 表達式,只要有一個信道有接收到數據,那麼 select 就結束,所以輸出如下

c2 received:  hello

2. 避免造成死鎖

select 在執行過程中,必須命中其中的某一分支。

如果在遍歷完所有的 case 後,若沒有命中(命中:也許這樣描述不太準確,我本意是想說可以執行信道的操作語句)任何一個 case 表達式,就會進入 default 裏的代碼分支。

但如果你沒有寫 default 分支,select 就會阻塞,直到有某個 case 可以命中,而如果一直沒有命中,select 就會拋出 deadlock 的錯誤,就像下面這樣子。

package main

import (
	"fmt"
)

func main() {
	c1 := make(chan string, 1)
	c2 := make(chan string, 1)

	// c2 <- "hello"

	select {
	case msg1 := <-c1:
		fmt.Println("c1 received: ", msg1)
	case msg2 := <-c2:
		fmt.Println("c2 received: ", msg2)
		// default:
		// 	fmt.Println("No data received.")
	}
}

運行後輸出如下

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [select]:
main.main()
        /Users/MING/GolandProjects/golang-test/main.go:13 +0x10f
exit status 2

解決這個問題的方法有兩種

一個是,養成好習慣,在 select 的時候,也寫好 default 分支代碼,儘管你 default 下沒有寫任何代碼。

package main

import (
	"fmt"
)

func main() {
	c1 := make(chan string, 1)
	c2 := make(chan string, 1)

  // c2 <- "hello"
  
	select {
	case msg1 := <-c1:
		fmt.Println("c1 received: ", msg1)
	case msg2 := <-c2:
		fmt.Println("c2 received: ", msg2)
	default:
		
	}
}

另一個是,讓其中某一個信道可以接收到數據

package main

import (
	"fmt"
	"time"
)

func main() {
	c1 := make(chan string, 1)
	c2 := make(chan string, 1)

  // 開啓一個協程,可以發送數據到信道
	go func() {
		time.Sleep(time.Second * 1)
		c2 <- "hello"
	}()

	select {
	case msg1 := <-c1:
		fmt.Println("c1 received: ", msg1)
	case msg2 := <-c2:
		fmt.Println("c2 received: ", msg2)
	}
}

3. select 隨機性

之前學過 switch 的時候,知道了 switch 裏的 case 是順序執行的,但在 select 裏卻不是。

通過下面這個例子的執行結果就可以看出

4. select 的超時

當 case 裏的信道始終沒有接收到數據時,而且也沒有 default 語句時,select 整體就會阻塞,但是有時我們並不希望 select 一直阻塞下去,這時候就可以手動設置一個超時時間。

package main

import (
	"fmt"
	"time"
)

func makeTimeout(ch chan bool, t int) {
	time.Sleep(time.Second * time.Duration(t))
	ch <- true
}

func main() {
	c1 := make(chan string, 1)
	c2 := make(chan string, 1)
	timeout := make(chan bool, 1)

	go makeTimeout(timeout, 2)

	select {
	case msg1 := <-c1:
		fmt.Println("c1 received: ", msg1)
	case msg2 := <-c2:
		fmt.Println("c2 received: ", msg2)
	case <-timeout:
		fmt.Println("Timeout, exit.")
	}
}

輸出如下

Timeout, exit.

5. 讀取/寫入都可以

上面例子裏的 case,好像都只從信道中讀取數據,但實際上,select 裏的 case 表達式只要求你是對信道的操作即可,不管你是往信道寫入數據,還是從信道讀出數據。

package main

import (
	"fmt"
)

func main() {
	c1 := make(chan int, 2)

	c1 <- 2
	select {
	case c1 <- 4:
		fmt.Println("c1 received: ", <-c1)
		fmt.Println("c1 received: ", <-c1)
	default:
		fmt.Println("channel blocking")
	}
}

輸出如下

c1 received:  2
c1 received:  4

6. 總結一下

select 與 switch 原理很相似,但它的使用場景更特殊,學習了本篇文章,你需要知道如下幾點區別:

  1. select 只能用於 channel 的操作(寫入/讀出),而 switch 則更通用一些;
  2. select 的 case 是隨機的,而 switch 裏的 case 是順序執行;
  3. select 要注意避免出現死鎖,同時也可以自行實現超時機制;
  4. select 裏沒有類似 switch 裏的 fallthrough 的用法;
  5. select 不能像 switch 一樣接函數或其他表達式。

系列導讀

01. 開發環境的搭建(Goland & VS Code)

02. 學習五種變量創建的方法

03. 詳解數據類型:****整形與浮點型

04. 詳解數據類型:byte、rune與string

05. 詳解數據類型:數組與切片

06. 詳解數據類型:字典與布爾類型

07. 詳解數據類型:指針

08. 面向對象編程:結構體與繼承

09. 一篇文章理解 Go 裏的函數

10. Go語言流程控制:if-else 條件語句

11. Go語言流程控制:switch-case 選擇語句

12. Go語言流程控制:for 循環語句

13. Go語言流程控制:goto 無條件跳轉

14. Go語言流程控制:defer 延遲調用

15. 面向對象編程:接口與多態

16. 關鍵字:make 和 new 的區別?

17. 一篇文章理解 Go 裏的語句塊與作用域

18. 學習 Go 協程:goroutine

19. 學習 Go 協程:詳解信道/通道

20. 幾個信道死鎖經典錯誤案例詳解

21. 學習 Go 協程:WaitGroup

22. 學習 Go 協程:互斥鎖和讀寫鎖

23. Go 裏的異常處理:panic 和 recover

24. 超詳細解讀 Go Modules 前世今生及入門使用

25. Go 語言中關於包導入必學的 8 個知識點

26. 如何開源自己寫的模塊給別人用?

27. 說說 Go 語言中的類型斷言?

28. 這五點帶你理解Go語言的select用法


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章