go语言中channel类型

目录

一、什么是channel

二、为什么要有channel

三、channel操作使用

初始化

操作

单向channel

双向channel,可读可写

四、close下什么场景会出现panic

五、总结


一、什么是channel

Channels are a typed conduit through which you can send and receive values with the channel operator, <-.

channel是go语言的核心类型之一,翻译为中文是"通道,管道",为了实现协程间的同步与通信。遵循FIFO(先进先出)的队列,保证线程安全。

二、为什么要有channel

"不要用共享内存来通信,而是使用通信来共享内存" -- go语言并发哲学

任何一种程序语言要实现并发能力,就要做好多线程之间的协调工作,让彼此知道对方状态,获取对方的信息,完成预定任务。大多数的编程语言的并发编程模型是基于线程和内存同步访问控制,go语言的并发编程的模型则用 goroutine 和 channel 来实现。channel是goroutine之间架了一条管道,在管道里传输数据,实现goroutine间的通信来协调工作(当然goroutine实现通信不止channel一种,还有 go语言中协程实现通信的三种方式 context\sync.cond\channel)。

在go语言中,CSP(Communicating Sequential Processes)模型是go语言并发编程哲学的实现(三种线程模型与CSP实现),goroutine与channel是CSP上层实现的两大基石。

channel的底层实现保证了协程操作安全:在任何同一时间内,channel中的一个数据只允许一个协程访问,不存在数据竞争。

三、channel操作使用

初始化

channel是引用类型,有带缓冲channel和无缓冲channel,未初始化的channel值是nil。通过内建函数make (仅对map\slice\channel初始化)分配内存并初始化。

操作

channel只有三种操作方式Send、Receive、close。

通过操作符 <- 实现发送或读取数据(中文社区更愿意把Send和接收,从通信操作符号看chan <- 是发送数据到chan,<- chan是接收chan中数据,这是从通信角度理解。从读写角度理解,我更愿意翻译为chan <- 为写入数据到chan,<-chan为从chan读取数据出来)。数据为go中任意类型:

close为关闭chan:close(chan)

虽然go语言采自动垃圾回收机制来管理内存,但go的垃圾回收器不会主动回收运行中的channel, 主动关闭channel为了防止内存泄露。

单向channel

单向channel分为write-only,read-only channel,主要作用有限制通信方向、减少竞态条件、提高代码可读性。

限制通信方向:使用只写通道可以在某些情况下限制通信的方向,确保特定的协程只能发送数据到通道,而不会在不适当的地方进行接收操作。这有助于清晰地定义协程之间的职责和交互。

减少竞态条件:当只有一个协程负责向通道发送数据,而其他协程只负责接收时,可以减少竞态条件的出现。这有助于避免数据竞争和复杂的同步问题。

提高代码可读性:通过使用只写通道,你可以在代码中清楚地表达协程的作用。这有助于其他开发人员更容易地理解代码并阅读文档。

Go 复制代码
unc worker(id int, jobs <-chan int, results chan<- int) {
   for job := range jobs {
      fmt.Printf("Worker %d started job %d\n", id, job)
      time.Sleep(time.Millisecond)
      fmt.Printf("Worker %d finished job %d\n", id, job)
      results <- job * 2
   }
}

func main() {
   numJobs := 5
   jobs := make(chan int, numJobs)
   results := make(chan int, numJobs)
   // 启动3个工作协程
   for i := 1; i <= 3; i++ {
      go worker(i, jobs, results)
   }
   // 向通道发送任务
   for j := 1; j <= numJobs; j++ {
      jobs <- j
   }
   close(jobs)
   // 收集结果
   for r := 1; r <= numJobs; r++ {
      result := <-results
      fmt.Println("Result:", result)
   }
}

在这个示例中,我们使用只写通道 chan<- 来传递任务给工作协程,工作协程的<- chan只负责从通道中接收任务。这种模式将任务分发和执行解耦,增加了代码的可读性和可维护性。

总之,单向channel在go语言中用于限制通道的使用方向,有助于提高代码的可读性、降低竞态条件和减少复杂性。

双向channel,可读可写

基本通道使用:创建一个通道,发送数据到通道,然后从通道接收数据

Go 复制代码
func base() {
   ch := make(chan int) // 创建一个通道
   go func() {
      ch <- 42 // 发送数据到通道
   }()
   value := <-ch                   // 从通道接收数据
   fmt.Println("Received:", value) // Received: 42
}

使用缓冲通道:创建带有缓冲区的通道,可以存储多个数据,然后使用循环向通道发送和接收数据。

Go 复制代码
func cacheChan() {
   ch := make(chan int, 2) // 创建一个容量为2的缓冲通道
   ch <- 1
   ch <- 2
   value1 := <-ch
   value2 := <-ch
   fmt.Println("Received:", value1, value2) // Received: 1 2
}

协程池: 使用通道来实现一个简单的协程池,从通道中获取任务并分发给协程进行处理。

Go 复制代码
func worker(id int, jobs <-chan int, results chan<- int) {
   for job := range jobs {
      fmt.Printf("Worker %d started job %d\n", id, job)
      time.Sleep(time.Millisecond)
      fmt.Printf("Worker %d finished job %d\n", id, job)
      results <- job * 2
   }
}

func goroutinePool() {
   numJobs := 5
   numWorkers := 3
   jobs := make(chan int, numJobs)
   results := make(chan int, numJobs)
   
   for i := 1; i <= numWorkers; i++ {
      go worker(i, jobs, results)
   }
   for j := 1; j <= numJobs; j++ {
      jobs <- j
   }
   close(jobs)

   var wg sync.WaitGroup
   wg.Add(numJobs)
   go func() {
      wg.Wait()
      close(results)
   }()

   for r := range results {
      fmt.Println("Result:", r)
      wg.Done()
   }
}

取消协程: 使用通道来实现协程的取消,通过发送信号告知协程停止工作。

Go 复制代码
func worker(cancel <-chan struct{}) {
   for {
      select {
      case <-cancel:
         fmt.Println("Worker canceled")
         return
      default:
         fmt.Println("Working...")
         time.Sleep(time.Second)
      }
   }
}

func cancelRoutine() {
   cancel := make(chan struct{})
   go worker(cancel)
   time.Sleep(3 * time.Second)
   fmt.Println("Canceling worker...")
   close(cancel)
   time.Sleep(1 * time.Second)
}

四、close下什么场景会出现panic

在使用channel时,为了获得良好的协程同步与通信结果,在一些场景下会导致程序panic,

如下为读写与channel状态对协程的影响表:

调用close关闭channel时,未初始化时关闭、重复关闭、关闭后发送、发送时关闭,在这四种情况下会出现panic:

未初始化就关闭

Go 复制代码
func main() {
   var ch chan int
   close(ch) // panic: close of nil channel
}

重复关闭

Go 复制代码
func main() {
   ch := make(chan int)
   close(ch)
   close(ch) // panic: close of closed channel
}

关闭后发送

Go 复制代码
func main() {
   wg := sync.WaitGroup{}
   wg.Add(1)
   ch := make(chan int)
   close(ch)

   go func() {
      defer wg.Done()
      ch <- 1 // panic: send on closed channel
   }()
   <-ch
   wg.Wait()
}

发送后关闭

Go 复制代码
func main() {
   ch := make(chan int)
   var wg sync.WaitGroup
   wg.Add(1)

   go func() {
      defer wg.Done()
      defer fmt.Println("close ch") // close ch
      defer close(ch)
      go func() {
         ch <- 1 // panic: send on closed channel
      }()
   }()
   fmt.Println(<-ch)
   wg.Wait()
}

不正确地关闭channel会导致程序panic,如何优雅关闭channel呢?

参看:《How to Gracefully Close Channels》

五、总结

本内容主要讲述了channel的基础知识,包括定义、使用背景、类型、操作方式、使用场景、不正确关闭channel会panic的场景。没有对channel的底层实现原理进行解读,参看channel的底层实现原理了解。

相关推荐
豆豆19 分钟前
为什么用PageAdmin CMS建设网站?
服务器·开发语言·前端·php·软件构建
落落落sss1 小时前
MybatisPlus
android·java·开发语言·spring·tomcat·rabbitmq·mybatis
简单.is.good1 小时前
【测试】接口测试与接口自动化
开发语言·python
Yvemil71 小时前
MQ 架构设计原理与消息中间件详解(二)
开发语言·后端·ruby
程序员是干活的2 小时前
私家车开车回家过节会发生什么事情
java·开发语言·软件构建·1024程序员节
我是陈泽2 小时前
一行 Python 代码能实现什么丧心病狂的功能?圣诞树源代码
开发语言·python·程序员·编程·python教程·python学习·python教学
优雅的小武先生2 小时前
QT中的按钮控件和comboBox控件和spinBox控件无法点击的bug
开发语言·qt·bug
虽千万人 吾往矣2 小时前
golang gorm
开发语言·数据库·后端·tcp/ip·golang
创作小达人2 小时前
家政服务|基于springBoot的家政服务平台设计与实现(附项目源码+论文+数据库)
开发语言·python
郭二哈2 小时前
C++——list
开发语言·c++·list