首页 > 编程笔记 > Go语言笔记 阅读:19

Golang select的用法(附带实例)

Go语言中,当有多个协程要对通道进行读和写操作时,通常会搭配 select 机制来实现。

select 是一种用于处理通道通信的语言结构。它允许一个协程同时等待多个通道操作,只要其中任何一个操作准备就绪,就立即处理。select 机制使得在 Go 语言中实现高效且简洁的并发编程变得更加容易。

select 的语法与 switch 的类似,每个 case 语句对应监听一个通道,且支持设置 default 选项。

示例代码如下:
//select的基本用法
select {
case <- ch1:
// 如果满足从通道ch1读数据,则执行该case块的操作
case ch2 <- 1:
// 如果满足向通道cha2写数据,则执行该case块的操作
default:
// 如果上面的操作都没有成功,则进入default处理流程
}
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 块中使用无限循环或者不加限制的等待操作,否则会导致主协程一直阻塞,从而影响程序的性能和稳定性。

相关文章