Golang select的用法(附带实例)
Go语言中,当有多个协程要对通道进行读和写操作时,通常会搭配 select 机制来实现。
select 是一种用于处理通道通信的语言结构。它允许一个协程同时等待多个通道操作,只要其中任何一个操作准备就绪,就立即处理。select 机制使得在 Go 语言中实现高效且简洁的并发编程变得更加容易。
select 的语法与 switch 的类似,每个 case 语句对应监听一个通道,且支持设置 default 选项。
示例代码如下:
下面是一个简单的示例代码,演示了如何使用 select 语句来处理通道通信。
在 select 语句中,使用了两个 case 块,分别用于读取通道 ch1 和 ch2 中的数据。default 块用于处理通道中没有数据的情况,以保证主协程不会一直阻塞在 select 语句上。
这段代码先是输出了一条“没有接收到数据”的消息,它在等待一秒后又继续检查通道中是否有数据。在两个协程都向通道写入 5 个数据后,select 语句退出。
我们可以利用 select 的这个特性做超时控制,比如使用 case <-time.After(time.Second * N) 创建一个控制超时的分支,在设置的时间间隔内,这个 case 不会被执行,当满足时间间隔条件时,则执行此 case 对应的逻辑。
示例代码如下:
在 Job1() 和 Job2() 函数中,模拟执行完逻辑后,向通道中发送代表执行完的信号。在 main() 函数中,for 循环使用 select 机制将从通道 ch1 和 ch2 读取数据作为 case 的条件,并设置了一个超时就退出 for 循环的时间节点。
由于定时器的时间 time.Millisecond*2 比协程 go Job2 向通道写入数据的时间 time.Millisecond*3 短,因此在这个例子中,select 语句会从定时器通道那里获取一个超时事件,并执行定时器通道的操作,打印输出 timeout 信息。
注意,这儿有一个陷阱,在 for + select 的代码中,如果 case 语句中出现 break 关键字,表示仅仅退出当前 select 机制,并不会退出整个 for 循环。因此,在这里使用了标签。
使用 select 时,还有一点需要注意,如果 case 后面跟的是表达式,那么表达式会被提前计算。
最后需要说明的是,select 语句执行时是非阻塞的,它只会执行准备就绪的 case 块中的操作,而不会像通常的读取或写入操作一样阻塞当前协程。这意味着在 select 语句执行的同时,其他协程可以继续并发地执行,这可以实现更高效的并发编程。
不过,在使用 select 语句时,应该尽可能地避免在 default 块中使用无限循环或者不加限制的等待操作,否则会导致主协程一直阻塞,从而影响程序的性能和稳定性。
select 是一种用于处理通道通信的语言结构。它允许一个协程同时等待多个通道操作,只要其中任何一个操作准备就绪,就立即处理。select 机制使得在 Go 语言中实现高效且简洁的并发编程变得更加容易。
select 的语法与 switch 的类似,每个 case 语句对应监听一个通道,且支持设置 default 选项。
示例代码如下:
//select的基本用法 select { case <- ch1: // 如果满足从通道ch1读数据,则执行该case块的操作 case ch2 <- 1: // 如果满足向通道cha2写数据,则执行该case块的操作 default: // 如果上面的操作都没有成功,则进入default处理流程 }select 机制的使用说明如下:
- select 的作用是监听多个通道上的操作,所以 case 后面必须是通道的读或写(即 <-ch 或 ch<-)操作,否则会报错;
- 执行 select 语句时,会按照从上到下的顺序依次检查每个 case 块;
- 当任意一个 case 块上的操作准备就绪时,select 语句执行该操作;
- 如果有多个 case 块上的操作同时准备就绪,则随机选择其中一个执行;
- select 语句中的 default 块是可选的。如果所有的 case 块都未准备就绪,select 语句就会执行 default 块中的操作。如果没有 default 块,那么 select 语句就会一直阻塞,直到有一个 case 块上的操作准备就绪为止;
- 不包含任何 case 的 select 语句会直接阻塞。
下面是一个简单的示例代码,演示了如何使用 select 语句来处理通道通信。
ch1 := make(chan int) //创建传输整数的通道ch1 ch2 := make(chan string) //创建传输字符串类型的通道ch2 //协程1:向通道ch1写入数据 go func() { for i := 1; i <= 5; i++ { time.Sleep(time.Second) ch1 <- i } }() //协程2:向通道ch2写入数据 go func() { for i := 1; i <= 5; i++ { time.Sleep(time.Second) ch2 <- fmt.Sprintf("data %d", i) } }() // for { select { case i := <-ch1: //读取通道ch1的case分支 fmt.Println("接收到int类型的数据", i, "from ch1") case s := <-ch2: //读取通道ch2的case分支 fmt.Println("接收到string类型的数据", s, "from ch2") default: //default分支 fmt.Println("没有接收到数据") time.Sleep(time.Second) } }在上面的示例中,我们创建了两个通道,分别用于传输整数和字符串类型的数据。然后使用两个协程分别向这两个通道中写入数据,并使用 select 语句在主协程中等待这两个通道的数据。
在 select 语句中,使用了两个 case 块,分别用于读取通道 ch1 和 ch2 中的数据。default 块用于处理通道中没有数据的情况,以保证主协程不会一直阻塞在 select 语句上。
这段代码先是输出了一条“没有接收到数据”的消息,它在等待一秒后又继续检查通道中是否有数据。在两个协程都向通道写入 5 个数据后,select 语句退出。
Go语言select与超时控制
除了读取、写入和关闭操作,select 语句还支持定时器操作。可以使用 time.After 函数创建一个定时器,该函数返回一个通道,当定时器超时时,此通道就会收到一个值。我们可以利用 select 的这个特性做超时控制,比如使用 case <-time.After(time.Second * N) 创建一个控制超时的分支,在设置的时间间隔内,这个 case 不会被执行,当满足时间间隔条件时,则执行此 case 对应的逻辑。
示例代码如下:
var ch1 = make(chan struct{}) var ch2 = make(chan struct{}) func Job1() { ch1 <- struct{}{} } func Job2() { time.Sleep(time.Millisecond*3) ch2 <- struct{}{} } func main() { go Job1() go Job2() myselect: for { select { case <-ch1: fmt.Println("job1 is running") case <-ch2: fmt.Println("job2 is running") case <-time.After(time.Millisecond*2): fmt.Println("timeout") break myselect } } }执行结果为:
job1 is running
在上述代码中,timeout 在 main() 函数中开启了 2 个协程,期望能正确执行完这 2 个协程。在 Job1() 和 Job2() 函数中,模拟执行完逻辑后,向通道中发送代表执行完的信号。在 main() 函数中,for 循环使用 select 机制将从通道 ch1 和 ch2 读取数据作为 case 的条件,并设置了一个超时就退出 for 循环的时间节点。
由于定时器的时间 time.Millisecond*2 比协程 go Job2 向通道写入数据的时间 time.Millisecond*3 短,因此在这个例子中,select 语句会从定时器通道那里获取一个超时事件,并执行定时器通道的操作,打印输出 timeout 信息。
注意,这儿有一个陷阱,在 for + select 的代码中,如果 case 语句中出现 break 关键字,表示仅仅退出当前 select 机制,并不会退出整个 for 循环。因此,在这里使用了标签。
使用 select 时,还有一点需要注意,如果 case 后面跟的是表达式,那么表达式会被提前计算。
最后需要说明的是,select 语句执行时是非阻塞的,它只会执行准备就绪的 case 块中的操作,而不会像通常的读取或写入操作一样阻塞当前协程。这意味着在 select 语句执行的同时,其他协程可以继续并发地执行,这可以实现更高效的并发编程。
不过,在使用 select 语句时,应该尽可能地避免在 default 块中使用无限循环或者不加限制的等待操作,否则会导致主协程一直阻塞,从而影响程序的性能和稳定性。