Golang 中的 Select


2022年1月30日, Learn eTutorial
2413

在本教程中,您将学习 select 关键字。要理解本教程,您应该对 Golang 中的通道Goroutine有扎实的知识。

Golang 中的 select 是什么?

在 Go 编程语言中,`select` 关键字允许您等待多个 Goroutines 的通信操作。Golang 中的 `select` 类似于 `switch` 语句。`select` 语句的语法遵循 `switch case` 模式。`select` 语句中的不同 `case` 会等待每个来自通道的发送或接收操作。
例如,考虑两个通道,假设是通道 A 和通道 B,它们之间进行一些发送和接收操作。它们还需要等待以实现并发和同步,这在 Golang 中通过 `select` 语句支持。

GO : Select

Golang 中如何声明 select?

`select` 关键字在 Golang 中定义 `select`。语法以 `select` 关键字开头,后跟花括号,其中包含一些 `case` 语句,再后跟一些表达式。
语法


Select {
Case <- first_channel (send or receive)
//block1
Case <- second _channel (send or receive)
//block2
default:
    //block3
}

你可以注意到 `select` 的结构与 `switch case` 类似。所以让我们通过讨论它们的区别来澄清。

GO : Select

select 和 switch case 的区别是什么?

select switch
在 Golang 中,Select Goroutines 等待多个通信操作。 在 Golang 中,switch 是一种表示 if-else 语句的方法。
Select 与通道和 Goroutines 一起使用。 switch 与具体数据类型一起使用。
从多个有效选项中随机选择一个 case。 在 Switch 中,每个 case 按顺序执行。
没有 fallthrough 概念 需要 fallthrough
Select 是非确定性的,因为你无法预测哪个 case 先运行。它随机选择一个 case。 Switch 是确定性的,即我们可以预测在 if 或 if-else 语句中哪个指令或块会执行。
select 语法 select { Case <- first_channel (发送或接收) //block1 Case <-second_channel (发送或接收) //block2 default: //block3 } Switch 语法 switch statement; expression { case expression1: //action case expression2: //action default: //action }

让我们看一个 `select` 和 `switch case` 的示例程序。


import "fmt"

func main() {
    ch := make(chan int, 4)
    ch <- 1
    ch <- 2
    ch <- 3
    ch <- 4
    close(ch)
    switch {
    //case <-ch: //  invalid case <-ch in switch (mismatched types int and bool)
    case <-ch == 1:
        fmt.Println("Firstswitch")
        fallthrough
    case <-ch == 2:
        fmt.Println("Secondswitch")
    }

第一次运行的输出 1

输出


Firstswitch
Secondswitch
Second_select receiver
 2

第二次运行的输出 2

输出


Firstswitch
Secondswitch
Second_select receiver
 2

Golang 中的 select 如何工作?

`select` 关键字选择一个通道必须准备好执行发送和接收操作的 `case`。当许多通道都准备好执行(即执行发送和接收操作)时,会随机选择一个 `case`。

让我们考虑一个程序


//PROGRAM USING SELECT KEYWORD
package main
import (
     "fmt"
     "time"
)

func main() {
  
  A := make(chan string)
  B := make(chan string)
  
  
  go func(){
  time.Sleep(2*time.Second)
  A <- "Hello "
  }()
  
  go func(){
  time.Sleep(1*time.Second)
  B <- "Haaiii"
  }()
  
  
  select {
  
  case rec1 :=  <-A:
   fmt.Println("I recived from channel A \n ",rec1)
  case rec2 :=  <-B:
   fmt.Println("I received from channel B \n ",rec2)
  
  }
  
}

输出


I received from channel B 
  Haaiii

在这个程序中,我们分别创建了两个通道,通道 A 和通道 B。我们已经在通道教程中讨论了通道的创建。因此,我们将直接进行,不再讨论通道概念


  A := make(chan string)     // created channel A 
  B := make(chan string)  // created channel b

该程序定义了两个匿名函数(没有名称的函数),它们将由两个Goroutines调用。`select` 关键字用于等待接收端,并等待通道(即通道 A 和通道 B)的接收端,具体取决于哪个通道首先返回或哪个通道首先包含信息,然后执行一些操作。


go func(){                           //anonymous functions
  time.Sleep(2*time.Second)
  A <- "Hello "
  }()
  
  go func(){
  time.Sleep(1*time.Second)
  B <- "Haaiii"
  }()

我们已经向通道 A 写入了信息 "hello",类似地,字符串 "Haaiii" 被发送到通道 B。程序中有两个函数,其中 A 等待 2 秒写入第一个,B 等待 1 秒。
`select` 关键字用于等待这两个通道的接收端。在 `select` 语句内部,有 `switch case` 语句,其中变量 `rec1` 分配了等待 A 的接收端,如果从通道 A 接收到一些信息,它就会被打印出来。
通道 B 也以类似的方式执行。上面显示的输出使概念更清晰。

select 何时会阻塞 Case 或 Channel?

如果 case 语句未准备好操作,`select` 关键字可以阻塞它们。

让我们通过一个例子来理解


package main
import "fmt"

func main() {
    channel1 := make(chan string)     //created channel1
    select {
    case receive := <-channel1:
        fmt.Println(receive)
    }
}

输出


fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
 /tmp/sandbox2985791004/prog.go:8 +0x36

Program exited.

上述 Go 程序创建了一个名为 channel1 的通道。仅使用 select 语句可以从 channel1 执行接收操作。在上述程序中,没有定义 Goroutines,如果没有 Goroutine,它无法执行发送操作。因此,在这个程序中,我们创建了 channel1,并且 select 正在等待从 channel1 接收,但由于缺少 Goroutines,所以无法接收。这种情况会导致死锁,这意味着 select 语句会切换到阻塞状态。

Golang 中为什么要选择 `select` 语句?

`select` 概念与通道和 Goroutine 紧密相关。Go 程序中的多个 Goroutine 发送数据并尝试通过通道进行通信。为了促进无堵塞通信,Golang 中包含了 `select` 功能。它控制通过通道执行的发送和接收操作,而不会出现任何堵塞。`select` 语句从一个通道并发接收数据,如果它处于就绪状态,则执行该数据。

注意:与通道和 Goroutines 绑定的 Select 语句在 Go 程序中实现同步和并发。

Select 中的 default case?

让我们通过上面会话中讨论的当 `select` 阻塞 `Case` 或 `Channel` 的例子来理解


package main
import "fmt"

func main() {
    channel1 := make(chan string)
    select {
    case receive := <-channel1:
        fmt.Println(receive)
   default:                                                    //default statement
        fmt.Println("Executes Default")
    }
}

输出


Executes Default

注意:与正常的 switch case 类似,如果 select 内部所有 case 均无效,则打印 default 语句。

select 中的超时机制

当使用 select 读取通道时,它一定会在一段时间后做其他事情,而不是一直阻塞在 select 中。下面是一个简单的例子。


package main

import (
    "fmt"
    "time"
)

func main() {
    timeout := make(chan bool, 1)
    go func() {
        time.Sleep(2 * time.Second)
        timeout <- true
    }()
    ch := make(chan int)
    select {
    case <-ch:
    case <-timeout:
        fmt.Println("timeout 01")
    case <-time.After(time.Second * 1):
        fmt.Println("timeout 02")
    }

输出


timeout 01

创建一个超时通道,这样其他地方就可以使用触发超时通道来完成 select 执行,或者还有另一种通过 time.After 机制来编写它的方法。

使用 `time.After` 机制的程序


package main

import (
    "fmt"
    "time"
)

func main() {
    timeout := make(chan bool, 1)
    go func() {
        time.Sleep(2 * time.Second)
        timeout <- true
    }()
    ch := make(chan int)
    select {
    case <-ch:
    case <-timeout:
        fmt.Println("timeout 01")
    case <-time.After(time.Second * 1):
        fmt.Println("timeout 02")
    }
}

输出


timeout 02

如何检查通道是否已满?

让我们看一个示例程序


package main

import (
    "fmt"
)

func main() {
    ch := make(chan int, 1)
    ch <- 1
    select {
    case ch <- 2:
        fmt.Println("channel value is", <-ch)
        fmt.Println("channel value is", <-ch)
    default:
        fmt.Println("channel blocking")
    }
}

输出


channel blocking

首先声明缓冲区大小为 1 的通道,然后放入值以填满通道。此时,可以使用 select + default 方法来确保通道是否已满。上面的示例将输出通道阻塞,我们将程序更改为以下内容:


package main

import (
    "fmt"
)

func main() {
    ch := make(chan int, 2)
    ch <- 1
    select {
    case ch <- 2:
        fmt.Println("channel value is", <-ch)
        fmt.Println("channel value is", <-ch)
    default:
        fmt.Println("channel blocking")
    }
}

将缓冲区大小更改为 2 后,您可以继续将值插入通道。

输出


channel value is 1
channel value is 2