通道声明和初始化

可以通过 chan类型关键字来声明通道类型变量:

  1. var ch chan int

上面这个表达式表示声明一个通道类型变量 ch,并且通道中只能传递 int类型数据。
与其他数据类型不同,通道类型变量除了声明通道类型本身外,还要声明通道中传递数据的类型,比如这里我们指定这个数据类型为 int。通道是类型相关的,我们必须在声明通道的时候同时指定通道中传递数据的类型,并且一个通道只能传递一种类型的数据,这一点和数组/切片类似。
我们还可以通过如下方式声明通道数组、切片、字典,以下声明方式表示chs中的元素都是 chan int 类型的通道:

  1. var chs [10]chan int
  2. var chs []chan int
  3. var chs map[string]chan int

不过,实际编码时,我们更多使用的是下面这种快捷方式同时声明和初始化通道类型:

  1. ch := make(chan int)

由于在 Go 语言中,通道也是引用类型(和切片、字典一样),所以可以通过 make函数进行初始化,在通过 make函数初始化通道时,还可以传递第二个参数,表示通道的容量:

  1. ch := make(chan int,10)
  1. 第二个参数是可选的,用于指定通道最多可以缓存多少个元素,默认值是 0,此时通道可以被称作非缓冲通道,`表示往通道中发送一个元素后,只有该元素被接收后才能存入下一个元素`,与之相对的,当缓存值大于 0 时,通道可以称作缓冲通道,即使通道元素没有被接收,也可以继续往里面发送元素,直到超过缓冲值,显然设置这个缓冲值可以提高通道的操作效率。

通道操作符

通道类型变量只支持发送和接收操作,即往通道中写入数据和从通道中读取数据,对应的操作符都是 <-,我们判断是发送还是接收操作的依据是通道类型变量位于 <- 左侧还是右侧,位于左侧是发送操作,位于右侧是接收操作:

  1. ch <- 1 // 往通道中写入数据 1
  2. x := <- ch // 从通道中读取数据并赋值给变量

当我们将数据发送到通道时,发送的是数据的副本,同理,从通道中接收数据时,接收的也是数据的副本。
发送和接收操作都是原子操作,同时只能进行发送或接收操作,不存在数据发送一半被接收,或者接收一半发送新数据的情况,并且两者都是阻塞的,如果通道中没有数据,进行读取操作的话会导致读取操作所在的协程阻塞,直到通道中写入了数据;反过来,如果通道中已经有了数据,再往里面写入数据的话,也会导致写入操作所在的协程阻塞,直到其中的数据被其他协程接收。

使用缓冲通道提升性能

上面这种情况发生在非缓冲通道中,对于缓冲通道,情况略有不同,假设 ch 是通过 make(chan int, 10) 进行初始化的通道,则其缓冲区大小是 10,这意味着,在没有被任何其他协程接收的情况下,我们可以一直往 ch 通道中写入 10 个数据,超过 10 个数据才会阻塞当前协程,直到通道被其他协程读取,显然,合理设置缓冲区可以提高通道的操作效率,尤其是在需要持续传输大量数据的场景。
我们可以通过如下示例代码简单测试下通道的缓冲机制:

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func test(ch chan int) {
  7. for i := 0; i < 100; i++ {
  8. ch <- i
  9. }
  10. close(ch)
  11. }
  12. func main() {
  13. start := time.Now()
  14. ch := make(chan int, 20)
  15. go test(ch)
  16. for v := range ch {
  17. fmt.Println(v)
  18. }
  19. end := time.Now()
  20. consume := end.Sub(start).String()
  21. fmt.Printf("consume %s s", consume)
  22. }

我们在主协程中初始化了一个带缓冲的通道,缓冲大小是 20,然后将其传递到子协程,并且在子协程中发送数据到通道,子协程执行完毕后,调用 close(ch) 显式关闭通道,这一行不能漏掉,否则主协程不知道子协程什么时候执行完毕,从一个空的通道接收数据会报如下运行时错误(死锁):

fatal error: all goroutines are asleep - deadlock!

关闭通道的操作只能执行一次,

  • 试图关闭已关闭的通道会引发 panic。
  • 此外,关闭通道的操作只能在发送数据的一方关闭,如果在接收一方关闭,会导致 panic,因为接收方不知道发送方什么时候执行完毕。
  • 向一个已经关闭的通道发送数据会导致 panic。

回到主协程,我们通过 i := range ch 循环从通道中读取数据,并将其打印出来。当通道关闭后会退出循环。