1. channel 底层结构

Golang 中的 channel 对应的底层结构为 hchan 结构体(channel的源码位置在Golang包的 runtime/chan.go):

如果 dataqsiz == 0 时,则为无缓冲 channel,如果 dataqsiz > 0 时,则为有缓冲 channel。

其中 recvq 和 sendq 是一个双向链表结构,链表中的元素为 sudog 结构体,其中该结构体中保存了g,所以本质上recvq 和 sendq 是保存了等待接收/发送数据的goroutine列表。

channel 中的 recvq 和 sendq 的使用场景如下所示:

在从 channel 接收数据时 (data := <- ch),如果 sendq 中没有等待发送数据的 goroutine,且 buf 中没有数据时,则需要把当前 goroutine 保存到 recvq 列表中,并挂起。

在向 channel 发送数据时 (ch <- data),如果 recvq 中没有等待接收数据的 goroutine,且 buf 满了的情况下,则需要把当前 goroutine 保存到 sendq 列表中,并挂起。

channel 结构图:

2. channel 的创建

创建 channel 的源码为runtime/chan.go文件中的 makechan 函数:

注意这里返回的是 hchan 的指针,因此我们在函数间可以直接传递 channel,而不用传递channel的指针了。

另外,因为channel 的内存分配都用到了 mallocgc 函数,而 mallocgc 是负责堆内存分配的关键函数,因此可见 channel 是分配在堆内存上的。

3. channel 的发送流程

channel 的发送:

ch <- data

channel 发送的源码对应 runtime/chan.go 的 chansend 函数:

full 函数,用于判断当前channel是否还有坑位接收待发送的数据:

send 函数,在recvq中有等待接收数据的goroutine时会被调用:www.yii666.com

总结 channel 的发送流程:

判断 channel 是否是 nil,如果是,则会永久阻塞导致死锁报错文章来源站点https://www.yii666.com/

如果 channel 中 recvq 存在接收者 goroutine,则直接把需要发送的数据拷贝到接收 goroutine,这里其实是有sodog 的结构,里面保存了接受者goroutine的指针。

如果 recvq 中不存在接收者:

a. 如果 buf 没有满,则直接把数据拷贝到 buf 的 sendx 位置

b. 如果 channel 为无缓冲 channel 或 buf 已满,则把当前 goroutine 保存到 sendq 等待队列中,阻塞当前 goroutine

4. channel 的接收流程

channel 的接收:

data := <- ch
data2, ok := <- ch

channel 的接收分别有2个函数,其中一种是带”ok“返回值的,另外一种是不带"ok"返回值的。文章来源地址https://www.yii666.com/blog/305634.html

  • 带”ok"返回值的函数,该返回的布尔值为 true 时,并不表示当前通道还没有关闭,而是仅仅表示当前获取到的值是通道的正常生产出来的数据,而不是零值;当该布尔值为 false 时,表示当前的通道已经被关闭,并且获取到的值是零值。
  • 不带"ok"返回值的函数,当 channel 被关闭时,就不能判断当前获取到的值是 channel 正常生产的值,还是零值了。

不管是否返回 received,channel 的接收都调用了 chanrecv 函数:

empty 函数用于判断从 channel c 中读取数据是否会阻塞:

recv 函数在 channel c 的 buf 是满的,且 sendq 中有等待发送的 goroutine 时会被调用:

总结 channel 的接收流程:

判断 channel 是否是 nil,如果是,则会永久阻塞导致死锁报错如果 channel 中 sendq 有等待发送数据的 goroutine:

a. 如果是无缓存 channel,则直接把要发送的数据拷贝到接收者的 goroutine 中,并唤醒发送方 goroutine;

b. 如果是有缓存的 channel(说明此时recvd满了),则把 buf 中的 recvx 位置的数据拷贝到当前接收的goroutine,然后把 sendq 中第一个等待发送goroutine的数据拷贝到buf 中的 sendx 位置,并唤醒发送的goroutine如果 channel 中 sendq 没有等待发送数据的 goroutine:

a. 如果 buf 有数据,则把 buf 中的 recvx 位置的数据拷贝到当前的接收goroutine

b. 如果 buf 没有数据,则把当前 goroutine 加入 recvd 等待队列中,并挂起

5. channel 使用注意事项

最后啰嗦一下 channel 使用的注意事项,这也是在我们平常开发中容易忽略的:

  • 一个 channel 不能多次 close,否则会导致 panic。
  • 关闭一个 nil 的 channel,会导致 panic。
  • 向一个已经 close 的 channel 发送数据,会导致 panic。
  • 不要从一个 receiver 测关闭 channel,也不要在有多个 sender 时关闭 channel。在go语言中,对于一个 channel,如果最终没有任何 goroutine 引用它,不管 channel 有没有被关闭,最终都会被 gc 回收。
  • 如果监听的channel 已经关闭,还可以获取到 channel buf 中剩余的值,当接收完 buf 中的数据后,才会获取到零值。