Golang并发编程——channel

channel

单纯地将函数并发执行是没有意义的。函数与函数之间需要交换数据才能体现并发执行函数的意义。

虽然可以使用共享内存进行数据交换，但是共享内存在不同的 goroutine 中容易发生竞态问题。为了保证数据交换的正确性，必须使用互斥量对内存进行加固，这种做法势必造成性能问题。

Go 语言的并发模型是 CSP（Communicating Sequential Processes），体长通过通信共享内存而不是通过共享内存实现通信。

如果说 goroutine 是 Go 程序并发的执行体，channel 就是他们之间的连接。channel 是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制。

Go 语言中的通道（channel）是一种特殊的类型。通道像一个传送带或者队列，总是遵循先进先出原则，保证收发数据的顺序。每一个通道都是一个具体类型的导管，也就是生命 channel 的时候需要为其指定元素类型。

channel 类型

channel 是一种类型，一种引用类型。声明通道类型的格式如下：

Copy
var 变量 chan 元素类型

举几个例子：

Copy
var ch1 chan int // 声明一个传递整型的通道
var ch2 chan bool // 声明一个传递布尔型的通道
var ch3 chan []int // 声明一个传递int切片的通道

创建 channel

通道是引用类型，通道类型的空值是 nil。

Copy
var ch chan int
fmt.Println(ch) // nil

声明的通道需要用 make 函数初始化才能使用。

创建 channel 的格式如下：

Copy
make(chan 元素类型,[缓冲大小])

channel 的缓冲大小是可选的

Copy
ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)

channel 操作

通道有发送（send）、接收（receive）和关闭（close）三种操作

发送和接收都使用 <- 符号。

现在我们先使用以下语句定义一个通道：

Copy
ch := make(chan int)

发送

将一个值发送到通道中。

Copy
ch <- 10

接收

从一个通道中接收值

Copy
x := <- ch // 从ch中接收值并赋值给变量x
<-ch // 从ch中接收值，忽略结果

关闭

我们通过调用内置的 close 函数来关闭通道

Copy
close(ch)

关于关闭通道需要注意的事情是，只有在通知接收方 goroutine 所有的数据都发送完成的时候才需要关闭通道。通道是可以被垃圾回收机制回收的，他和关闭文件是不一样的，在结束操作之后关闭文件是必须要做的，但关闭通道不是必须的。

关闭后的通道有以下特点：

Copy
对一个关闭的通道再发送值就会导致panic。
对一个关闭的通道进行接收会一直获取值直到通道为空。
对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
关闭一个已经关闭的通道会导致panic。

无缓冲的通道

无缓冲的通道又称为阻塞的通道。我们来看一下下面的代码

Copy
func main() {
ch := make(chan int)
ch <- 10
fmt.Println("发送成功")
}

上面那段代码能够通过编译，但是执行的时候会出现以下错误

Copy
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:

main.main()
.../src/github.com/pprof/studygo/day06/channel02/main.go:8 +0x54

为什么会出现 deadlock 错误呢？

因为我们使用 ch := make (chan int) 创建的是无缓冲的通道，无缓冲的通道只有在有人接收值的时候才能发送值。就像你住的小区没有快递柜和代收点，快递员给你打电话必须要把这个物品送到你手上，简单来说就是无缓冲的通道必须要有接收才能发送。

上面的代码会阻塞在 ch <- 10 这一行代码形成死锁，如何解决这个问题呢？

一种方法是启用一个 goroutine 来接收值，例如：

Copy
func recv(c chan int) {
ret := <-c
fmt.Println("接收成功", ret)
}

func main() {
ch := make(chan int)
go recv(ch) // 启用goroutine从通道接收值
ch <- 10
fmt.Println("发送成功")
}

有缓冲的通道

解决上面问题的方法还有一种就是使用有缓冲区的通道。

我们可以在使用 make 函数初始化通道的时候为其指定通道的容量，例如：

Copy
func main() {
ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
ch <- 10
fmt.Println("发送成功")
}

只要通道的容量大于零，那么该通道就是有缓冲的通道，通道的容量表示通道中能存放元素的数量。就像你小区的快递柜只有这么多格子，满了就装不下，阻塞了，等别人取走一个快递员就可以放一个。

close()

可以通过内置的 close () 函数关闭 channel（如果你的管道不往里面存值或者取值的时候一定记得关闭管道）

Copy
package main

import "fmt"

func main() {
c := make(chan int)
go func() {
for i := 1; i < 5; i++ {
c <- 1
}
close(c)
}()
for {
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}
fmt.Println("main结束")
}

如何优雅的从通道循环取值

当通过通道发送有限的数据时，我们可以通过 close 函数关闭通道来告知从该通道接收值的 goroutine 停止等待。当通道被关闭时，往该通道发送值会引发 panic，从该通道里接收的值一直都是类型零值。那如何判断一个通道是否被关闭了呢？

我们来看下面这个例子：

Copy
// channel 练习
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
// 开启goroutine将0~100的数发送到ch1中
go func() {
for i := 0; i < 100; i++ {
ch1 <- i
}
close(ch1)
}()
// 开启goroutine从ch1中接收值，并将该值的平方发送到ch2中
go func() {
for {
i, ok := <-ch1 // 通道关闭后再取值ok=false
if !ok {
break
}
ch2 <- i * i
}
close(ch2)
}()
// 在主goroutine中从ch2中接收值打印
for i := range ch2 { // 通道关闭后会退出for range循环
fmt.Println(i)
}
}

从上面的例子中我们看到有两种方式在接收值的时候判断通道是否被关闭，我们通常使用的是 for range 的方式。

单向通道

有的时候我们会将通道作为参数在多个任务函数间传递，很多时候我们在不同的任务函数中使用通道都会对其进行限制，比如限制通道在函数中只能发送或只能接收。

Go 语言中提供了单向通道来处理这种情况。例如，我们把上面的例子改造如下：

Copy
func counter(out chan<- int) {
for i := 0; i < 100; i++ {
out <- i
}
close(out)
}

func squarer(out chan<- int, in <-chan int) {
for i := range in {
out <- i * i
}
close(out)
}
func printer(in <-chan int) {
for i := range in {
fmt.Println(i)
}
}

func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go counter(ch1)
go squarer(ch2, ch1)
printer(ch2)
}

其中，

Copy
chan <- int 是一个只能发送的通道，可以发送但是不能接收；
<-chan int 是一个只能接收的通道，可以接收但是不能发送。

在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的，但反过来不可以。

常见异常

注意：关闭已经关闭的 channel 也会引发 panic。