学会 Go select 语句,轻松实现高效并发

简介: 本文主要介绍了 `Go` 语言中的 `select` 语句。先是介绍语法,然后根据示例介绍了基本用法,接着介绍与channel结合使用的场景,最后总结使用的注意事项。

耐心和持久胜过激烈和狂热。

哈喽大家好,我是陈明勇,本文介绍的内容是 Goselect 语句。如果本文对你有帮助,不妨点个赞,如果你是 Go 语言初学者,不妨点个关注,一起成长一起进步,如果本文有错误的地方,欢迎指出!

前言

Go 语言中,GoroutineChannel 是非常重要的并发编程概念,它们可以帮助我们解决并发编程中的各种问题。关于它们的基本概念和用法,前面的文章 一文初探 Goroutine 与 channel 中已经进行了介绍。而本文将重点介绍 select,它是协调多个 channel 的桥梁。

select 介绍

什么是 select

selectGo 语言中的一种控制结构,用于在多个通信操作中选择一个可执行的操作。它可以协调多个 channel 的读写操作,使得我们能够在多个 channel 中进行非阻塞的数据传输、同步和控制。

为什么需要 select

Go 语言中的 select 语句是一种用于多路复用通道的机制,它允许在多个通道上等待并处理消息。相比于简单地使用 for 循环遍历通道,使用 select 语句能够更加高效地管理多个通道。

以下是一些 select 语句的使用场景:

  • 等待多个通道的消息(多路复用)
    当我们需要等待多个通道的消息时,使用 select 语句可以非常方便地等待这些通道中的任意一个通道有消息到达,从而避免了使用多个goroutine进行同步和等待。
  • 超时等待通道消息
    当我们需要在一段时间内等待某个通道有消息到达时,使用 select 语句可以与 time 包结合使用实现定时等待。
  • 在通道上进行非阻塞读写
    在使用通道进行读写时,如果通道没有数据,读操作或写操作将会阻塞。但是使用 select 语句结合 default 分支可以实现非阻塞读写,从而避免了死锁或死循环等问题。

因此,select 的主要作用是在处理多个通道时提供了一种高效且易于使用的机制,简化了多个 goroutine 的同步和等待,使程序更加可读、高效和可靠。

select 基础

语法

select {
    case <- channel1:
        // channel1准备好了
    case data := <- channel2:
        // channel2准备好了,并且可以读取到数据data
    case channel3 <- data:
        // channel3准备好了,并且可以往其中写入数据data
    default:
        // 没有任何channel准备好了
}
复制代码

其中, <- channel1 表示读取 channel1 的数据,data <- channel2 表示用 data 去接收数据;channel3 <- data 表示往 channel3 中写入数据。

select 的语法形式类似于 switch,但是它只能用于 channel 操作。在 select 语句中,我们可以定义多个 case,每个 case 都是一个 channel 操作,用于读取或写入数据。如果有多个 case 同时可执行,则会随机选择其中一个。如果没有任何可执行的 case,则会执行 default 分支(如果存在),或者阻塞等待直到至少有一个 case 可执行为止。

基本用法

package main
import (
   "fmt"
   "time"
)
func main() {
   ch1 := make(chan int)
   ch2 := make(chan int)
   go func() {
      time.Sleep(1 * time.Second)
      ch1 <- 1
   }()
   go func() {
      time.Sleep(2 * time.Second)
      ch2 <- 2
   }()
   for i := 0; i < 2; i++ {
      select {
      case data, ok := <-ch1:
         if ok {
            fmt.Println("从 ch1 接收到数据:", data)
         } else {
            fmt.Println("通道已被关闭")
         }
      case data, ok := <-ch2:
         if ok {
            fmt.Println("从 ch2接收到数据: ", data)
         } else {
            fmt.Println("通道已被关闭")
         }
      }
   }
   select {
   case data, ok := <-ch1:
      if ok {
         fmt.Println("从 ch1 接收到数据:", data)
      } else {
         fmt.Println("通道已被关闭")
      }
   case data, ok := <-ch2:
      if ok {
         fmt.Println("从 ch2接收到数据: ", data)
      } else {
         fmt.Println("通道已被关闭")
      }
   default:
      fmt.Println("没有接收到数据,走 default 分支")
   }
}
复制代码

执行结果

从 ch1 接收到数据: 1
从 ch2接收到数据:  2
没有接收到数据,走 default 分支
复制代码

上述示例中,首先创建了两个 channelch1ch2,分别在不同的 goroutine 中向两个 channel 中写入数据。然后,在主 goroutine 中使用 select 语句监听两个channel,一旦某个 channel 上有数据流动,就打印出相应的数据。由于 ch1 中的数据比 ch2 中的数据先到达,因此首先会打印出 "从 ch1 接收到数据: 1",然后才打印出 "从 ch2接收到数据:  2"

为了方便测试 default 分支,我写了两个 select 代码块,执行到第二个 select 代码块的时候,由于 ch1ch2 都没有数据了,因此执行 default 分支,打印 "没有接收到数据,走 default 分支"

一些使用 select 与 channel 结合的场景

实现超时控制

package main
import (
   "fmt"
   "time"
)
func main() {
   ch := make(chan int)
   go func() {
      time.Sleep(3 * time.Second)
      ch <- 1
   }()
   select {
   case data, ok := <-ch:
      if ok {
         fmt.Println("接收到数据: ", data)
      } else {
         fmt.Println("通道已被关闭")
      }
   case <-time.After(2 * time.Second):
      fmt.Println("超时了!")
   }
}
复制代码

执行结果为:超时了!

在这个例子中,程序将在 3 秒后向 ch 通道里写入数据,而我在 select 代码块里设置的超时时间为 2 秒,如果在 2 秒内没有接收到数据,则会触发超时处理。

实现多任务并发控制

package main
import (
   "fmt"
)
func main() {
   ch := make(chan int)
   for i := 0; i < 10; i++ {
      go func(id int) {
         ch <- id
      }(i)
   }
   for i := 0; i < 10; i++ {
      select {
      case data, ok := <-ch:
         if ok {
            fmt.Println("任务完成:", data)
         } else {
            fmt.Println("通道已被关闭")
         }
      }
   }
}
复制代码

执行结果(每次执行的顺序都会不一致):

任务完成: 1
任务完成: 5
任务完成: 2
任务完成: 3
任务完成: 4
任务完成: 0
任务完成: 9
任务完成: 6
任务完成: 7
任务完成: 8
复制代码

在这个例子中,启动了 10 个 goroutine 并发执行任务,并使用一个 channel 来接收任务的完成情况。在主函数中,使用 select 语句监听这个 channel,每当接收到一个完成的任务时,就进行处理。

监听多个通道的消息

package main
import (
   "fmt"
   "time"
)
func main() {
   ch1 := make(chan int)
   ch2 := make(chan int)
   // 开启 goroutine 1 用于向通道 ch1 发送数据
   go func() {
      for i := 0; i < 5; i++ {
         ch1 <- i
         time.Sleep(time.Second)
      }
   }()
   // 开启 goroutine 2 用于向通道 ch2 发送数据
   go func() {
      for i := 5; i < 10; i++ {
         ch2 <- i
         time.Sleep(time.Second)
      }
   }()
   // 主 goroutine 从 ch1 和 ch2 中接收数据并打印
   for i := 0; i < 10; i++ {
      select {
      case data := <-ch1:
         fmt.Println("Received from ch1:", data)
      case data := <-ch2:
         fmt.Println("Received from ch2:", data)
      }
   }
   fmt.Println("Done.")
}
复制代码

执行结果(每次执行程序打印的顺序都不一致):

Received from ch2: 5
Received from ch1: 0
Received from ch1: 1
Received from ch2: 6
Received from ch1: 2
Received from ch2: 7
Received from ch1: 3
Received from ch2: 8
Received from ch1: 4
Received from ch2: 9
Done.
复制代码

该示例代码中,通过使用 select 多路复用,可以同时监听多个通道的数据,并避免了使用多个 goroutine 进行同步和等待的问题。

使用 default 实现非阻塞读写

import (
   "fmt"
   "time"
)
func main() {
   ch := make(chan int, 1)
   go func() {
      for i := 1; i <= 5; i++ {
         ch <- i
         time.Sleep(1 * time.Second)
      }
      close(ch)
   }()
   for {
      select {
      case val, ok := <-ch:
         if ok {
            fmt.Println(val)
         } else {
            ch = nil
         }
      default:
         fmt.Println("No value ready")
         time.Sleep(500 * time.Millisecond)
      }
      if ch == nil {
         break
      }
   }
}
复制代码

执行结果(每次执行程序打印的顺序都不一致):

No value ready
1
No value ready
2
No value ready
No value ready
3
No value ready
No value ready
4
No value ready
No value ready
5
No value ready
No value ready
复制代码

这个代码中,使用了 default 分支来实现非阻塞的通道读取和写入操作。在 select 语句中,如果有通道已经准备好进行读写操作,那么就会执行相应的分支。但是如果没有任何通道准备好读写,那么就会执行 default 分支中的代码。

select 的注意事项

以下是关于 select 语句的一些注意事项:

  • select 语句只能用于通信操作,如 channel 的读写,不能用于普通的计算或函数调用。
  • select 语句会阻塞,直到至少有一个 case 语句满足条件。 如果有多个 case 语句满足条件,则会随机选择一个执行。
  • 如果没有 case 语句满足条件,并且有 default 语句,则会执行 default 语句。
  • select 语句中使用 channel 时,必须保证 channel 是已经初始化的。
  • 如果一个通道被关闭,那么仍然可以从它中读取数据,直到它被清空,此时会返回通道元素类型的零值和一个布尔值,指示通道是否已关闭。

总之,在使用 select 语句时,要仔细考虑每个 case 语句的条件和执行顺序,避免死锁和其他问题。

总结

本文主要介绍了 Go 语言中的 select 语句。在文章中,首先介绍了 select 的基本概念,包括它是一种用于在多个通道之间进行选择的语句,以及为什么需要使用 select

接下来,文章详细介绍了 select 的基础知识,包括语法和基础用法。在语法方面,讲解了 select 语句的基本结构以及如何使用 case 子句进行通道选择。在基础用法方面,介绍了如何使用 select 语句进行通道的读取和写入操作,并讲解了一些注意事项。

在接下来的内容中,文章列举了一些使用 selectchannel 结合的场景。这些场景包括实现超时控制、实现多任务并发控制、监听多个通道的消息以及使用 default 实现非阻塞读写。对于每个场景,文章都详细介绍了如何使用 select 语句实现。

最后,文章总结了 select 的注意事项,包括选择的通道必须是可读或可写的通道、select 语句中的 case 子句必须是通道操作或者空的 default 子句,不能是其他类型的语句等等。


作者:陈明勇

链接:https://juejin.cn/post/7210005376653377597

来源:稀土掘金

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

目录
相关文章
|
14天前
|
人工智能 Go 调度
掌握Go并发:Go语言并发编程深度解析
掌握Go并发:Go语言并发编程深度解析
|
14天前
|
安全 Java Go
Java vs. Go:并发之争
【4月更文挑战第20天】
27 1
|
14天前
|
供应链 Go
掌握Go语言:利用Go语言的单向通道和select语句,提升库存管理效率(21)
掌握Go语言:利用Go语言的单向通道和select语句,提升库存管理效率(21)
|
14天前
|
Go 调度 开发者
CSP模型与Goroutine调度的协同作用:构建高效并发的Go语言世界
【2月更文挑战第17天】在Go语言的并发编程中,CSP模型与Goroutine调度机制相互协同,共同构建了高效并发的运行环境。CSP模型通过通道(channel)实现了进程间的通信与同步,而Goroutine调度机制则确保了并发任务的合理调度与执行。本文将深入探讨CSP模型与Goroutine调度的协同作用,分析它们如何共同促进Go语言并发性能的提升。
|
14天前
|
程序员 Go
Golang深入浅出之-Select语句在Go并发编程中的应用
【4月更文挑战第23天】Go语言中的`select`语句是并发编程的关键,用于协调多个通道的读写。它会阻塞直到某个通道操作可行,执行对应的代码块。常见问题包括忘记初始化通道、死锁和忽视`default`分支。要解决这些问题,需确保通道初始化、避免死锁并添加`default`分支以处理无数据可用的情况。理解并妥善处理这些问题能帮助编写更高效、健壮的并发程序。结合使用`context.Context`和定时器等工具,可提升`select`的灵活性和可控性。
30 2
|
14天前
|
存储 算法 编译器
掌握Go语言:探索Go语言递归函数的高级奥秘,优化性能、实现并发、解决算法难题(28)
掌握Go语言:探索Go语言递归函数的高级奥秘,优化性能、实现并发、解决算法难题(28)
|
14天前
|
SQL Go 数据库
【Go语言专栏】Go语言中的事务处理与并发控制
【4月更文挑战第30天】Go语言在数据库编程中支持事务处理和并发控制,确保ACID属性和多用户环境下的数据完整性。`database/sql`包提供事务管理,如示例所示,通过`Begin()`、`Commit()`和`Rollback()`执行和控制事务。并发控制利用Mutex、WaitGroup和Channel防止数据冲突。结合事务与并发控制,开发者可处理复杂场景,实现高效、可靠的数据库应用。
|
14天前
|
Cloud Native Go 云计算
多范式编程语言Go:并发与静态类型的结合
Go语言是Google于2007年开发的开源编程语言,旨在提高程序开发和部署的效率。它的独特特征在于结合了并发处理与静态类型系统,提供了简洁、高效、并行处理能力的编程体验。本文将探讨Go语言的特点、应用场景以及其在现代软件开发中的优势。
|
14天前
|
安全 Go
Golang深入浅出之-Go语言中的并发安全队列:实现与应用
【5月更文挑战第3天】本文探讨了Go语言中的并发安全队列,它是构建高性能并发系统的基础。文章介绍了两种实现方法:1) 使用`sync.Mutex`保护的简单队列,通过加锁解锁确保数据一致性;2) 使用通道(Channel)实现无锁队列,天生并发安全。同时,文中列举了并发编程中常见的死锁、数据竞争和通道阻塞问题,并给出了避免这些问题的策略,如明确锁边界、使用带缓冲通道、优雅处理关闭以及利用Go标准库。
28 5
|
14天前
|
存储 缓存 安全
Golang深入浅出之-Go语言中的并发安全容器:sync.Map与sync.Pool
Go语言中的`sync.Map`和`sync.Pool`是并发安全的容器。`sync.Map`提供并发安全的键值对存储,适合快速读取和少写入的情况。注意不要直接遍历Map,应使用`Range`方法。`sync.Pool`是对象池,用于缓存可重用对象,减少内存分配。使用时需注意对象生命周期管理和容量控制。在多goroutine环境下,这两个容器能提高性能和稳定性,但需根据场景谨慎使用,避免不当操作导致的问题。
40 4