Golang之select介绍

403 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第11天,点击查看活动详情

一、概述

select 是 Golang 中的一个控制结构,语法上类似于switch 语句,只不过select是用于 goroutine 间通信的 ,每个 case 必须是一个通信操作,要么是发送要么是接收,select 会随机执行一个可运行的 case。如果没有 case 可运行,goroutine 将阻塞,直到有 case 可运行。

二、基本语法

语法

//select基本用法
select {
case <- chan1:
// 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行该case处理语句
default:
// 如果上面都没有成功,则进入default处理流程
}

注意

  • 如果没有default分支,select会阻塞在多个channel上,对多个channel的读/写事件进行监控。

  • 如果有一个或多个IO操作可以完成,则Go运行时系统会随机的选择一个执行,否则的话,如果有default分支,则执行default分支语句,如果连default都没有,则select语句会一直阻塞,直到至少有一个IO操作可以进行。 

三、Select使用

3.1 同时从多个通道中接收数据

import (
	"fmt"
	"time"
)

func main() {

	//1.定义一个通道 10个数据int
	intChan := make(chan int, 10)
	for i := 0; i < 10; i++ {
		intChan <- i
	}
	//2.定义一个通道 5个数据string
	stringChan := make(chan string, 5)
	for i := 0; i < 5; i++ {
		stringChan <- "hello" + fmt.Sprintf("%d", i)
	}
	//使用select来获取channel里面的数据的时候不需要关闭channel
	for {
		select {
		case v := <-intChan:
			fmt.Printf("从 intChan 读取的数据%d\n", v)
			time.Sleep(time.Millisecond * 50)
		case v := <-stringChan:
			fmt.Printf("从 stringChan 读取的数据%v\n", v)
			time.Sleep(time.Millisecond * 50)
		default:
			fmt.Printf("数据获取完毕")
			return //注意退出...
		}
	}

}

运行结果

从 stringChan 读取的数据hello0
从 stringChan 读取的数据hello1
从 stringChan 读取的数据hello2
从 intChan 读取的数据0
从 stringChan 读取的数据hello3
从 intChan 读取的数据1
从 stringChan 读取的数据hello4
从 intChan 读取的数据2
从 intChan 读取的数据3
从 intChan 读取的数据4
从 intChan 读取的数据5
从 intChan 读取的数据6
从 intChan 读取的数据7
从 intChan 读取的数据8
从 intChan 读取的数据9
数据获取完毕

3.2 造成死锁

当程序中不使用default 分支,如果一直没有命中其中的某个 case 最后会造成死锁。

import "fmt"

func main() {
	// 创建三个通道
	ch1 := make(chan string, 1)
	ch2 := make(chan string, 1)
	ch3 := make(chan string, 1)

	select {
	case message1 := <-ch1: // 如果从通道 1 收到数据
		fmt.Println("ch1 received:", message1)
	case message2 := <-ch2: // 如果从通道 2 收到数据
		fmt.Println("ch2 received:", message2)
	case message3 := <-ch3: // 如果从通道 3 收到数据
		fmt.Println("ch3 received:", message3)
	}
}

运行报错

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [select]:
main.main()
	D:/Codes/gocode/godemo/base11/test02.go:11 +0xfd

运行上面的程序会造成死锁。解决该问题的方法是写好 default 分支。

空 select造成死锁

package main

func main() {
	select {}	//fatal error: all goroutines are asleep - deadlock!
}

select 语句没有任何 case,因此它会一直阻塞,导致死锁。

3.3 select 超时处理

case 里的通道始终没有接收到数据时,而且也没有 default 语句时, select 整体就会阻塞,但是有时我们并不希望 select 一直阻塞下去,这时候就可以手动设置一个超时时间。

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)
	c3 := 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 msg3 := <-c3:
		fmt.Println("c3 received: ", msg3)
	case <-timeout:
		fmt.Println("Timeout, exit.")
	}
}

运行结果

Timeout, exit.

总结

  1. select语句只能用于信道的读写操作;
  2. select中的case条件(非阻塞)是并发执行的,select会选择先操作成功的那个case条件去执行,如果多个同时返回,则随机选择一个执行,此时将无法保证执行顺序。对于阻塞的case语句会直到其中有信道可以操作,如果有多个信道可操作,会随机选择其中一个 case 执行;
  3. 对于case条件语句中,如果存在信道值为nil的读写操作,则该分支将被忽略,可以理解为从select语句中删除了这个case语句;
  4. 如果有超时条件语句,判断逻辑为如果在这个时间段内一直没有满足条件的case,则执行这个超时case。如果此段时间内出现了可操作的case,则直接执行这个case。一般用超时语句代替了default语句;
  5. 对于空的select{},会引起死锁;
  6. 对于for中的select{}, 也有可能会引起cpu占用过高的问题。

附参考文章链接

www.go-edu.cn/2022/05/27/…