今天咱们总结的对象是:通道Channel!
一、通道是什么?
作为Go语言最有特色的数据类型,通道(channel)完全可以与协程(goroutine)并驾齐驱,共同代表 Go 语言独有的并发编程模式CSP(Communicating Sequential Processes)和编程哲学。
Don’t communicate by sharing memory; share memory by communicating.
不要通过共享内存来通信,而应该通过通信来共享内存。
通道类型完美诠释了这个编程理念的后半句:通过通信来共享内存!通过通道,我们可以在多个 goroutine 之间传递数据。
当然,高并发是Go最具有自身特色的特性,所以我们将在第六章用一章的篇幅来详细总结高并发原理,今天这篇我们只看作为数据类型本身而言,通道的一些特性和需要注意的知识点。
通道(channel)是一种特殊的引用类型(零值为nil),它本身就是并发安全的(Go语言自带的唯一一个满足并发安全性的类型)。每个channel都有一个特殊的类型,也就是channel可发送数据的类型。
  //声明一个传递布尔型的通道
  //声明的通道后需要使用make函数初始化之后才能使用
  var ch1 chan bool 
  // 创建一个int类型的通道
  ch2 := make(chan int)
  // 创建一个string切片类型带有缓冲的通道,通道容量为5
  ch3 := make(chan []string, 5) 
          二、通道的常见操作
作为goroutine之间的数据“沟通通道”,那可以想象到对于通道来说,一共有三种操作:发送(send)、接收(receive)和关闭(close)!另外对于有缓冲通道(下面讲到),还有两个常见操作获取元素数量(len)和通道容量(cap),平时使用场景非常少(因为在并发程序中该信息会随着接收操作而失效,但是它对某些故障诊断和性能优化会有帮助)。
- 发送:发送语句将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine
 - 接收:接收就是从一个通道中接收值,可以付给变量,也可以忽略
 - 关闭:使用内置的close函数可以关闭一个channel。对基于已关闭的channel的任何发送和再关闭操作都将导致panic异常,但是依旧可以接收,会接收到零值。另外,和关闭文件不同,通道关闭是可选的,因为通道是可以被垃圾回收机制回收的
 - 获取通道内元素数量:使用内置的len函数
 - 获取通道容量:使用内置的cap函数
 
  //发送
  ch <- x 
  // 接收
  x = <-ch
  // 接收,但忽略接收到的值
  <-ch 
  // 关闭
  close(ch) 
  //判断通道关闭的方法
  //通道关闭后再取值ok=false
  //但是这个值会有延迟,比如通道还有数据但已经关闭时,ok还是true
  i, ok := <-ch1 
  if !ok {
      ……
  }
  //通道关闭后会退出for range循环
  for i := range ch2 { 
      fmt.Println(i)
  }
          一个通道相当于一个先进先出(FIFO)的队列。可以看到上面的接送操作符<-的方向完美的展示出了“消息的去向”。
对于通道的基本操作,有以下基本特性:
- 对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的
 - 发送操作和接收操作中对元素值的处理都是不可分割的
 - 发送操作在完全完成之前会被阻塞。接收操作也是如此
 
需要强调的是,进入通道的并不是在接收操作符右边的那个元素值,而是它的副本,所以就是两步操作,复制元素副本,把副本放到通道里。而从通道出来时,实际上也有两步操作:第一步是生成正在通道中的这个元素值的副本,并准备给到接收方,第二步是删除在通道中的这个元素值。上面特性的第二条也就是在强调这“发送”和“接收”里面的“子操作”都是一气呵成不可打断的。
三、通道分类
- 无缓冲通道:又称为阻塞的通道,或同步通道,因为使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。就像在没有快递柜之前,快递员必须打电话把快递包裹亲手交给你一样,是必须同步进行的。
 - 有缓冲的通道:和无缓冲通道不同的是,它有一个可以缓冲指定大小(称为容量)的空间“暂时存放”要传递的值。这时候就好比小区里安装了一个快递柜,能存放100个包裹,假如还有空位,快递小哥就会电话告诉你把包裹放在几号柜子里,你有时间来取就可以了。
 - 单向通道:被限制了只能发送或接收的通道。主要是用来约束其他代码的行为。在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的。
 
                
                  func squarer(out chan<- int, in <-chan int) {
      for i := range in {
          out <- i * i
      }
      close(out)
  }
          
          上面代码中chan<- int是一个只能发送的通道,可以发送但是不能接收;<-chan int是一个只能接收的通道,可以接收但是不能发送。
这里有一个优良实践,有些消息事件并不携带额外的信息,它仅仅是用作两个goroutine之间的同步,这时候我们可以用struct{}空结构体作为channels元素的类型,虽然也可以使用bool或int类型实现同样的功能,done <- 1语句也比done <- struct{}{}更短,但是空结构体带来的性能开销更小。
四、通道引起阻塞
了解了通道操作的基本特性之后我们可以发现,基于这些特性,在对通道进行操作的时候就可能会产生代码阻塞。那都在哪些场景下可能会产生长时间的代码阻塞呢?
正常使用通道过程中可能产生的阻塞:
- 对于有缓冲通道:
 - 通道已满:对它的所有发送操作都会被阻塞,直到通道中有元素值被接收走
 - 通道已空:对它的所有接收操作都会被阻塞,直到通道中有新的元素值出现
 - 对于无缓冲通道:无论是发送操作还是接收操作,一开始执行就会被阻塞,直到配对的操作也开始执行,才会继续传递。因为它用的是“同步”方式传递数据(想想上面接收快递的场景)。并且,数据是直接从发送方复制到接收方的,中间并不会用非缓冲通道做中转。
 
  func main() {
    pipline := make(chan string)
    pipline <- "hello world"
    fmt.Println(<-pipline)
  }
  //fatal error: all goroutines are asleep - deadlock! 
  //上面的代码就是无缓冲通道,在接受者未准备好的情况下就发送会造成死锁
  //解决方法可以是先让接收者执行
  //当然也可以换成有缓冲通道
  func main() {
    pipline := make(chan string)
    fmt.Println(<-pipline)
    pipline <- "hello world"
  } 
          
          错误使用通道过程中可能产生的阻塞:
- 对于值为nil的通道:对它的发送操作和接收操作都会永久地处于阻塞状态。它们所属的 goroutine 中的任何代码,都不再会被执行。所以一定要记得用make初始化通道之后在使用!
 
五、通道引起panic
对通道的操作何时会引起panic,我们特别做一下总结:
- 通道已关闭:对它的发送和关闭操作,都会引起panic
 
对于通道类型的大致内容就总结到这里,你都Get了吗?
