> For the complete documentation index, see [llms.txt](https://sunwenfei.gitbook.io/sunwenfei/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://sunwenfei.gitbook.io/sunwenfei/golang/golang-ji-chu-jiao-cheng/bing-fa/guan-dao-channel.md).

# 管道(channel)

上一节我们介绍了Golang中如何使用`goroutine`实现并发，本节我们介绍下`channel`，即`管道`，来看看Golang中是怎么使用`channel`来进行`goroutine`之间的通信的。

#### 什么是`channel`

`channel`可以看作是`goroutine`互相通信的管道。就像水可以从管道的一端流向另一端，数据也可以从`channel`的一端流向另一端。

#### 声明`channel`

每个`channel`都需要绑定一个数据类型，这个数据类型就是`channel`可以传输的数据的类型，`channel`不允许传输其他任何数据类型。

一个`channel`的类型声明如下：

```go
chan T
```

`channel`的零值为`nil`。`nil`channel没有任何用处，我们需要使用`make`来初始化一个`channel`。

```go
package main

import "fmt"

func main() {
    var a chan int
    if a == nil {
        fmt.Println("channel a is nil, going to define it")
        a = make(chan int)
        fmt.Printf("Type of a is %T", a)
    }
}
```

这里声明的`channel`未手动初始化，因此自动初始化为零值`nil`。后面的判断条件`a == nil`成立后执行`if`语句内部的语句。我们使用`make(chan int)`创建了一个`chan int`类型的`channel`。该程序输出如下：

```
channel a is nil, going to define it
Type of a is chan int
```

当然，我们也可以声明`channel`的同时进行手动初始化：

```go
a := make(chan int)
```

#### 发送 & 接收数据

从`channel`中发送、接收数据的语法如下：

```go
data := <- a // read from channel a
a <- data // write to channel a
```

箭头的指向形象的表明了是从`channel`中接收数据，还是向`challel`发送数据。`data := <- a`中箭头从`channel`指出，表示从`channel` `a`中读取数据并赋值给变量`data`。`a <- data`中箭头指向一个`channel`，因此是向`channel` `a`中写入数据`data`。

#### 向`channel`发送、从`channel`接收数据会阻塞`goroutine`执行

向`channel`发送数据，以及从`channel`接收数据会阻塞当前`goroutine`的执行。这句话怎么理解呢？其实很简单，无论我们是向`channel`发送数据，还是从`channel`接收数据，无非是通过类似于`data := <- a`或者`a <- data`的语句来完成，而语句本身的执行肯定是位于一个`goroutine`内。如果执行的是`a <- data`，即向一个`channel`发送数据，这时执行流会阻塞，直到有其他`goroutine`从这个`channel`中读取了刚刚写入的数据。类似的，如果执行的是`data := <- a`，即从一个`channel`中读取数据，这时执行流也也会阻塞，直到有其他`goroutine`向这个`channel`中写入数据。

`channel`就是基于这个特性而不是类似于其他编程语言中的锁来实现不同`goroutine`之间的高效通信的。

#### `channel`使用举例

下面我们通过例子来看下`goroutine`是如何使用`channel`进行通信的。

我们首先还用上一节的那个简单例子：

```go
package main

import (
    "fmt"
    "time"
)

func hello() {
    fmt.Println("Hello world goroutine")
}
func main() {
    go hello()
    time.Sleep(1 * time.Second)
    fmt.Println("main function")
}
```

该程序使用了`time.Sleep`来让主协程等待`hello`协程的执行，下面我们改用正规做法，即使用`channel`来同步主协程和`hello`子协程的执行：

```go
package main

import (
    "fmt"
)

func hello(done chan bool) {
    fmt.Println("Hello world goroutine")
    done <- true
}
func main() {
    done := make(chan bool)
    go hello(done)
    <-done
    fmt.Println("main function")
}
```

这里我们通过`done := make(chan bool)`创建了一个`channel`，该`channel`的类型为`chan bool`，即支持传递`bool`类型的数据。我们将该`channel`作为入参传给了`hello`协程，因此该协程内部可以给该`channel`传递数据。注意我们开启协程之后的一行代码：`<-done`。这行代码是从`done` `channel`内读取数据，因此会阻塞主协程的执行，直到该`channel`内有数据能够读到为止，直到读到数据主协程才会继续执行。

这里的`<-done`仅仅等待`done` `channel`有数据可读，并没有将读到的数据赋值给某个变量，这是完全合法的。比如这里我们仅仅是为了找到同步点，而并不需要读到什么数据。

协程`hello`内部先打印了`Hello world goroutine`，然后向`done` `channel`内部写入了一个`bool`类型的数据`true`，这时主协程收到了这个数据，不再被阻塞，接着打印`main function`。

该程序的执行结果如下，注意是稳定输出：

```
Hello world goroutine
main function
```

我们再修改下前面的程序，在`hello`协程里面加个`sleep`，让`hello`协程的执行时间变长点，看看主协程会不会等待该子协程向`channel`内写入数据。

```go
package main

import (
    "fmt"
    "time"
)

func hello(done chan bool) {
    fmt.Println("hello go routine is going to sleep")
    time.Sleep(4 * time.Second)
    fmt.Println("hello go routine awake and going to write to done")
    done <- true
}
func main() {
    done := make(chan bool)
    fmt.Println("Main going to call hello go goroutine")
    go hello(done)
    <-done
    fmt.Println("Main received data")
}
```

这里我们在`hello`协程内`sleep`了四秒钟才向`channel`内写入了数据，由于`<-done`是阻塞的，因此仍然需要等待`channel`中写入数据，该程序执行结果如下，也是稳定输出：

```
Main going to call hello go goroutine
hello go routine is going to sleep
hello go routine awake and going to write to done
Main received data
```

下面我们再举个例子，以更好的理解`goroutine`和`channel`。这个程序用来计算一个数子的各个位的平方和，各个位的立方和，再将二者加起来。比如输出是123，那么算法如下：

> squares = (1 *1) + (2* 2) + (3 *3) cubes = (1* 1 *1) + (2* 2 *2) + (3* 3 \* 3) output = squares + cubes = 50

我们实现上将平方和、立方和的计算放到不同的协程内去执行，在主协程内等待两个协程执行完成后再将两个协程的计算结果想加。

```go
package main

import "fmt"

func calcSquares(number int, squareop chan int) {
    sum := 0
    for number != 0 {
        digit := number % 10
        sum += digit * digit
        number /= 10
    }
    squareop <- sum
}

func calcCubes(number int, cubeop chan int) {
    sum := 0
    for number != 0 {
        digit := number % 10
        sum += digit * digit * digit
        number /= 10
    }
    cubeop <- sum
}

func main() {
    number := 589
    sqrch := make(chan int)
    cubech := make(chan int)
    go calcSquares(number, sqrch)
    go calcCubes(number, cubech)
    squares, cubes := <-sqrch, <-cubech
    fmt.Println("Final output", squares+cubes)
}
```

这里`squares, cubes := <-sqrch, <-cubech`会等待两个协程将计算结果写到对应的`channel`内。该程序执行结果如下：

```
Final output 1536
```

#### 死锁

使用`channel`时需要注意别陷入`死锁`状态。如果某个`goroutine`向某个`channel`发送数据，那么需要某个其他`goroutine`接收该`channel`的数据，否则会发送运行时错误：`deadlock`。类似的，如果某个`goroutine`正在等待从某个`channel`中接收数据，那么需要某个其他`goroutine`向该`channel`中写入数据，否则也会发送运行时错误：`deadlock`。

```go
package main

func main() {
    ch := make(chan int)
    ch <- 5
}
```

这里我们创建了一个`channel`ch，类型为`chan int`。然后我们向该`channel`中写入了一个值`5`。但是在该程序中并没有其他`goroutine`接收该`channel`中的数据。因此会报运行时错误：

```
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
        /Users/goWorkspace/src/test/main.go:5 +0x55
exit status 2
```

#### 可读写`channel`、读`channel`、写`channel`

前面我们讨论的`channel`都是既可读、又可写的。我们也可以创建仅支持可读，或仅支持可写的`channel`。

```go
package main

import "fmt"

func sendData(sendch chan<- int) {
    sendch <- 10
}

func main() {
    sendch := make(chan<- int)
    go sendData(sendch)
    fmt.Println(<-sendch)
}
```

这里我们创建了一个写`channel` `sendch`，从箭头指向也能看出来这是一个写`channel`。`fmt.Println(<-sendch)`尝试从该`channel`中读取数据，这是不允许的，Golang不允许从写`channel`中读取数据，因此报以下错误：

```
./main.go:12:14: invalid operation: <-sendch (receive from send-only type chan<- int)
```

尽然`channel`是用来做通信、数据传输用的，那么仅支持可写、或仅支持可读有什么意义呢？答案是`channel 转换`。我们可以将`读写channel`转换为`读channel`或者`写channel`；但反之不行，不能将`读channel`或者`写channel`转换为`读写channel`。

```go
package main

import "fmt"

func sendData(sendch chan<- int) {
    sendch <- 10
}

func main() {
    chnl := make(chan int)
    go sendData(chnl)
    fmt.Println(<-chnl)
}
```

这里我们首先创建了一个`读写channel` `chnl`，然后将该`读写channel`传给`goroutine` `sendData`，`sendData`通过参数将该`读写channel`转换为了`写channel`，因此`sendData`内部该`channel`是仅可写的，而在`main channel`中是既可读又可写的。该程序输出如下：`10`。

#### 关闭`channel`

`channel`的发送方有权关闭`channel`，关闭`channel`会通知数据接收方，告知对方数据已发送完成，没有后续数据了。

数据接收方可通过如下方式来判断`channel`是否已经关闭：

```go
v, ok := <-ch
```

如果是正常的数据发送，则`ok`为`true`，如果`ch`被数据发送方关闭了，则`ok`为`false`，并且数据`v`接收到的值为对应数据类型的默认零值。

```go
package main

import "fmt"

func producer(chnl chan int) {
    for i := 0; i < 10; i++ {
        chnl <- i
    }
    close(chnl)
}
func main() {
    ch := make(chan int)
    go producer(ch)
    for {
        v, ok := <-ch
        if ok == false {
            break
        }
        fmt.Println("Received ", v, ok)
    }
}
```

这里的`producer`协程向`channel`中写入了0到9，接着关闭了`channel`。主函数通过一个无限循环来不断的读取`channel`中的数据，通过`ok`字段来判断`channel`是否关闭，如果为`false`则表示关闭。该程序执行结果如下：

```
Received  0 true
Received  1 true
Received  2 true
Received  3 true
Received  4 true
Received  5 true
Received  6 true
Received  7 true
Received  8 true
Received  9 true
```

#### for range

前面使用无限for循环来从`channel`中读取数据，Golang提供了一种更加方便的方式来不断的读取`channel`中的数据，直到`channel`关闭。

```go
package main

import "fmt"

func producer(chnl chan int) {
    for i := 0; i < 10; i++ {
        chnl <- i
    }
    close(chnl)
}
func main() {
    ch := make(chan int)
    go producer(ch)
    for v := range ch {
        fmt.Println("Received ", v)
    }
}
```

这里`for range`语句从`channel` `ch`中不断读取数据，直到`channel`关闭。该程序输出如下：

```
Received  0
Received  1
Received  2
Received  3
Received  4
Received  5
Received  6
Received  7
Received  8
Received  9
```

我们使用`for range`重构一下前面计算平方和、立方和的程序，细心的话会发现前面`calcSquares`函数和`calcCubes`函数中有几行数字循环的样板代码，我们使用`for range`重构下：

```go
package main

import "fmt"

func digits(number int, dchnl chan int) {
    for number != 0 {
        digit := number % 10
        dchnl <- digit
        number /= 10
    }
    close(dchnl)
}
func calcSquares(number int, squareop chan int) {
    sum := 0
    dch := make(chan int)
    go digits(number, dch)
    for digit := range dch {
        sum += digit * digit
    }
    squareop <- sum
}

func calcCubes(number int, cubeop chan int) {
    sum := 0
    dch := make(chan int)
    go digits(number, dch)
    for digit := range dch {
        sum += digit * digit * digit
    }
    cubeop <- sum
}

func main() {
    number := 589
    sqrch := make(chan int)
    cubech := make(chan int)
    go calcSquares(number, sqrch)
    go calcCubes(number, cubech)
    squares, cubes := <-sqrch, <-cubech
    fmt.Println("Final output", squares+cubes)
}
```

我们将样板代码抽成了一个函数`digits`，在`calcSquares`函数和`calcCubes`函数中分别创建了协程来不断输出迭代出来的数字，直到协程被关闭。该程序输出结果为：

```
Final output 1536
```
