select是golang中的控制语句,和switch有点类似,但是使用场景和原理却是完全不同,使用select配合channel可以实现协程之间的通信,也可以实现io层面的超时控制,也可以实现对于并发的控制
一、语法特点
1、select中的每个case都必须是一个通道
2、多个case中的通道,哪个符合就执行哪个通道,如果没有没有符合的case,要看是否有设置了default,如果有设置了default,就执行default中的逻辑,如果没有设置default,就会一直阻塞至其中的case被执行
二、实现协程之间的通信
子协程向主协程通信
如下,main函数中定义了一个select,其中设置了一个通道的case,从ch通道中获取数据。
main函数执行到select会阻塞,直到获取通道的值,这时候通过子协程,对ch进行赋值,所以会先打印child,后打印select start
ch := make(chan int)
go func() {
fmt.Println("child")
time.Sleep(2 * time.Second)
ch <- 1
}()
select {
case res := <-ch:
fmt.Println("select start")
fmt.Println(res)
}
输出如下
子协程向其他子协程通信
这边定义了一个子协程,对childCh通道写数据,另一个子协程在for无限循环中不断的获取childCh通道的值,如果有值,则打印得到的值
childCh := make(chan int)
go func() {
for {
select {
case res := <-childCh:
fmt.Println("child2")
fmt.Println(res)
}
}
}()
go func() {
fmt.Println("child")
time.Sleep(2 * time.Second)
childCh <- 1
}()
for {
time.Sleep(time.Second)
}
输出如下:
多个子协程向主函数通信
如下,定义了两个通道,select会在两个通道随机选择,哪个通道先准备好了数据,就执行哪个通道
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
fmt.Println("child2")
ch2 <- 2
}()
go func() {
fmt.Println("child1")
ch1 <- 1
}()
for i := 0; i < 2; i++ {
select {
case res1 := <-ch1:
fmt.Println("ch1")
fmt.Println(res1)
case res2 := <-ch2:
fmt.Println("ch2")
fmt.Println(res2)
}
}
以下,主函数main中定义了两个通道,select定义在for循环中,主函数不断的从ch1和ch2通道中获取数据,如果没有数据的话,则执行default,两个子协程则是不断的往通道中分别写入 from 1和from 2数据。
// 定义两个通道
ch1 := make(chan string)
ch2 := make(chan string)
// 启动两个 goroutine,分别从两个通道中获取数据
go func() {
for {
ch1 <- "from 1"
}
}()
go func() {
for {
ch2 <- "from 2"
}
}()
// 使用 select 语句非阻塞地从两个通道中获取数据
for {
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
default:
// 如果两个通道都没有可用的数据,则执行这里的语句
fmt.Println("no message received")
}
}
三、实现io的超时控制
这边使用的是select+channel+time.After实现了超时控制,这边列的只是简单的demo,io逻辑可以类似如下代码,放在do函数中,当io请求时间超过了time.After定义的时间,就会输出timeout
package main
import (
"fmt"
"time"
)
func main() {
// 创建一个可以取消的上下文
// 定义两个通道
ch := make(chan string)
go do(ch)
select {
case res := <-ch:
fmt.Println("res")
fmt.Println(res)
case <-time.After(time.Second * 2):
fmt.Println("timeout")
}
}
func do(ch chan string) {
time.Sleep(3 * time.Second)
ch <- "done"
}
四、实现并发控制
适用场景:
比如某个逻辑getHttp中的逻辑只能允许一定的并发执行,当并发过高的时候会拖垮其中的服务或者外部服务时,我们就需要对并发进行控制。
这边创建了一个最多只能存放2个值的semaphoreBig通道,当并发到来的时候,通过semaphoreBig 信号量进行控制只能同时处理2个并发,多余的并发会阻塞至有空余的信号量后才会执行。
package main
import (
"context"
"fmt"
"github.com/gin-gonic/gin"
)
var semaphoreBig = make(chan struct{}, 2) // 并发控制信号量
func main() {
r := gin.Default() //创建gin
r.Use(func(c *gin.Context) {
//处理请求
c.Next()
})
r.GET("/", index) //绑定路由
r.Run(":8001") //运行绑定端口
}
func index(c *gin.Context) {
Do(context.TODO())
}
func Do(ctx context.Context) {
select {
case <-ctx.Done():
fmt.Println("ctx done")
default:
var releaseSemaphore func()
semaphoreBig <- struct{}{}
releaseSemaphore = func() {
<-semaphoreBig // 释放信号量
}
func() {
defer releaseSemaphore()
getHttp()
}()
}
}
// 执行相应的http逻辑
func getHttp() {
fmt.Println("http")
}