企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
[TOC] ### **什么是channel** ***** #### **概述** go channel 存在3种状态 * nil 未初始化的状态,只进行了声明 * active 正常的channel,可读或者可写 * closed 已关闭,**关闭后的channel != nil** ![Gmvb6O.png](https://s1.ax1x.com/2020/03/30/Gmvb6O.png) >有种特殊情况,当nil的通道在select的某个case中时,这个case会阻塞,但不会造成死锁。 channel可进行`3种操作`: 1. 读 2. 写 3. 关闭 #### **基本读写操作** 定义一个channel: ``` ch := make(chan int) ``` 将一个数据写入(发送)至channel: ``` ch <- value ``` 向channel写入数据通常会导致程序阻塞,直到有其他goroutine从这个channel中读取数据。从 channel中读取数据的语法是: ``` value := <-ch ``` >如果channel之前没有写入数据,那么从channel中读取数据也会导致程序阻塞,直到channel中被写入数据为止。我们之后还会提到如何控制channel只接受写或者只允许读取,即单向channel。 ` ` ### **channel的使用场景** ***** channel用在**数据流动的地方**: 1. 消息传递、消息过滤 2. 信号广播 3. 事件订阅与广播 4. 请求、响应转发 5. 任务分发 6. 结果汇总 7. 并发控制 8. 同步与异步 9. ... ### **channel的基本操作和注意事项** ***** #### **使用for range读channel** * 场景:当需要不断从channel读取数据时 * 原理:使用`for-range`读取channel,这样既安全又便利,当channel关闭时,for循环会自动退出,无需主动监测channel是否关闭,可以防止读取已经关闭的channel,造成读到数据为通道所存储的数据类型的零值。 * 用法: ```go for x := range ch{ fmt.Println(x) } ``` #### **select原理** ``` select { case <-chan1: // 如果chan1成功读到数据,则进行该case处理语句 case chan2 <- 1: // 如果成功向chan2写入数据,则进行该case处理语句 default: // 如果上面都没有成功,则进入default处理流程 } ``` #### **使用select处理多个channel** * 场景:需要对多个通道进行同时处理,但**只处理最先发生的channel时** * 原理:`select`可以同时监控多个通道的情况,只处理未阻塞的case。**当通道为nil时,对应的case永远为阻塞,无论读写。特殊关注:普通情况下,对nil的通道写操作是要panic的**。 * 用法: ``` var wg sync.WaitGroup func main() { c := make(chan int, 2) d := make(chan string, 2) wg.Add(1) go func() { defer wg.Done() c <- 1 d <- "joker" }() select { case x := <- c: fmt.Print(x) case y := <- d: fmt.Print(y) } wg.Wait() } //output 1 ``` ` ` #### **只读/只写channel** 只读channel只能读取,只写channel只能写入,更易读,更安全 ``` 只写通道:chan<- T 只读通道:<-chan T ``` * 场景:协程对某个通道只读或只写时 * 目的:A. 使代码更易读、更易维护,B. 防止只读协程对通道进行写数据,但通道已关闭,造成panic。 * 用法: * 如果协程对某个channel只有写操作,则这个channel声明为只写。 * 如果协程对某个channel只有读操作,则这个channe声明为只读 ``` // 只有generator进行对outCh进行写操作,返回声明 // <-chan int,可以防止其他协程乱用此通道,造成隐藏bug func generator(int n) <-chan int { outCh := make(chan int) go func(){ for i:=0;i<n;i++{ outCh<-i } }() return outCh } // consumer只读inCh的数据,声明为<-chan int // 可以防止它向inCh写数据 func consumer(inCh <-chan int) { for x := range inCh { fmt.Println(x) } } ``` ` ` #### **使用缓冲channel增强并发** 创建一个带缓冲的channel: ``` c := make(chan int, 1024) ``` 在调用make()时将缓冲区大小作为第二个参数传入即可,比如上面这个例子就创建了一个大小为1024的int类型channel,即使没有读取方,写入方也可以一直往channel里写入,在缓冲区被填完之前都不会阻塞。 ` ` 从带缓冲的channel中读取数据可以使用与常规非缓冲channel完全一致的方法,但我们也可以使用range关键来实现更为简便的循环读取: ``` for i := range c { fmt.Println("Received:", i) } ``` ` ` #### **超时机制** 在并发编程的通信过程中,**最需要处理的就是超时问题**,即**向channel写数据时发现channel已满**,或者**从channel试图读取数据时发现channel为空**。如果不正确处理这些情况,很**可能会导致整个goroutine锁死**。 ` ` Go语言没有提供直接的超时处理机制,但我们可以利用select机制。虽然select机制不是专为超时而设计的,却能很方便地解决超时问题。因为select的特点是只要其中一个case已经完成,程序就会继续往下执行,而不会考虑其他case的情况: ``` // 首先,我们实现并执行一个匿名的超时等待函数 timeout := make(chan bool, 1) go func() { time.Sleep(1e9) // 等待1秒钟 timeout <- true }() // 然后我们把timeout这个channel利用起来 select { case <-ch: // 从ch中读取到数据 case <-timeout: // 一直没有从ch中读取到数据,但从timeout中读取到了数据 } ``` 这样使用select机制可以避免永久等待的问题,因为程序会在timeout中获取到一个数据后继续执行,无论对ch的读取是否还处于等待状态,从而达成1秒超时的效果。 ` ` >这种写法看起来是一个小技巧,但却是在Go语言开发中避免channel通信超时的最有效方法。在实际的开发过程中,这种写法也需要被合理利用起来,从而有效地提高代码质量。 ` ` #### **channel传递** 在Go语言中channel本身也是一个**原生类型**,**与map之类的类型地位一样**,因此channel本身在定义后也可以通过channel来传递。 我们可以使用这个特性来实现Linux上非常常见的管道(pipe)特性。管道也是使用非常广泛的一种设计模式,比如在处理数据时,我们可以采用管道设计,这样可以比较容易以插件的方式增加数据的处理流程。 ` ` #### **单向channel** 假如一个channel真的只能读,那么肯定只会是空的,因为你没机会往里面写数据。同理,如果一个channel只允许写,即使写进去了,也没有丝毫意义,因为没有机会读取里面的数据。所谓的单向channel概念,其实只是对channel的一种使用限制。 ` ` 在将一个channel变量传递到一个函数时,可以通过将其指定为单向channel变量,从而限制 该函数中可 以对此 channel的操作, 比如只能往 这个 channel写,或者只 能从这个channel读. 单向channel变量的声明非常简单,如下: ``` var ch1 chan int // ch1是一个正常的channel,不是单向的 var ch2 chan<- float64// ch2是单向channel,只用于写float64数据 var ch3 <-chan int // ch3是单向channel,只用于读取int数据 ``` 基于ch4,我们通过类型转换初始化了两个单向channel:单向读的ch5和单向写的ch6。 ` ` 为什么要做这样的限制呢?从设计的角度考虑,所有的代码应该都遵循“最小权限原则”,从而避免没必要地使用泛滥问题,进而导致程序失控。 单向channel的用法: ``` func Parse(ch <-chan int) { for value := range ch { fmt.Println("Parsing value", value) } } ``` 除非这个函数的实现者无耻地使用了类型转换,否则这个函数就不会因为各种原因而对ch进行写,避免在ch中出现非期望的数据,从而很好地实践最小权限原则。 ` ` #### **关闭channel** 关闭channel的语句非常简单 ``` close(ch) ``` #### **使用`_,ok`判断channel是否关闭** * 场景:读channel,但不确定channel是否关闭时 * 原理:读已关闭的channel会得到零值,如果不确定channel,需要使用`ok`进行检测。ok的结果和含义: * `true`:读到数据,并且通道没有关闭。 * `false`:通道关闭,无数据读到。 * 用法: ``` if v, ok := <- ch; ok { fmt.Println(v) } ```