内部结构
Go语言channel是first-class的,意味着它可以被存储到变量中,可以作为参数传递给函数,也可以作为函数的返回值返回。作为Go语言的核心特征之一,虽然channel看上去很高端,但是其实channel仅仅就是一个数据结构而已,结构体定义如下:
struct Hchan
{
uintgo qcount; // 队列q中的总数据数量
uintgo dataqsize; // 环形队列q的数据大小
uint16 elemsize; // 当前使用量
bool closed; // 关闭标志
uint8 elemalign;
Alg* elemalg; // interface for element type
uintgo sendx; // 发送index
uintgo recvx; // 接收index
WaitQ recvq; // 因recv而阻塞的等待队列
WaitQ sendq; // 因send而阻塞的等待队列
Lock;
};
可能会有人疑惑,结构体中只看到了队列大小相关的域,并没有看到存放数据的域啊?如果是带缓冲区的chan,则缓冲区数据实际上是紧接着Hchan结构体中分配的。
c = (Hchan*)runtime.mal(n + hint*elem->size);
另一个重要部分就是recvq和sendq两个链表,一个是因读这个通道而导致阻塞的goroutine,另一个是因为写这个通道而阻塞的goroutine。如果一个goroutine阻塞于channel了,那么它就被挂在recvq或sendq中。WaitQ是链表的定义,包含一个头结点和一个尾结点:
struct WaitQ
{
SudoG* first;
SudoG* last;
};
队列中的每个成员是一个SudoG结构体变量。
struct SudoG
{
G* g; // g and selgen constitute
uint32 selgen; // a weak pointer to g
SudoG* link;
int64 releasetime;
byte* elem; // data element
};
该结构中主要的就是一个g和一个elem。elem用于存储goroutine的数据。读通道时,数据会从Hchan的队列中拷贝到SudoG的elem域。写通道时,数据则是由SudoG的elem域拷贝到Hchan的队列中。
读写channel操作
空通道是指将一个channel赋值为nil,或者定义后不调用make进行初始化。按照Go语言的语言规范,读写空通道是永远阻塞的。其实在函数runtime.chansend和runtime.chanrecv开头就有判断这类情况,如果发现参数c是空的,则直接将当前的goroutine放到等待队列,状态设置为waiting。
对已关闭的chan读写
- 写入会panic
- 可重复读取,永远不会阻塞,并返回一个通道数据类型的零值,这个实现也很简单,将零值复制到调用函数的参数ep中
- 如果有值在队列里,则ok=true,否则为false (可解除所有阻塞)消耗的资源又少执行的速度又快
对未初始化的chan读写:读写未初始化的 chan 都会阻塞
- 写
- 未初始化的
chan
此时是等于nil
,当它不能阻塞的情况下,直接返回false
,表示写chan
失败 - 当
chan
能阻塞的情况下,则直接阻塞gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
, 然后调用throw(s string)
抛出错误,其中waitReasonChanSendNilChan
就是刚刚提到的报错"chan send (nil chan)"
- 读
- 未初始化的
chan
此时是等于nil
,当它不能阻塞的情况下,直接返回false
,表示读chan
失败 - 当
chan
能阻塞的情况下,则直接阻塞gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
, 然后调用throw(s string)
抛出错误,其中waitReasonChanReceiveNilChan
就是刚刚提到的报错"chan receive (nil chan)"
- 关闭一个空通道,也会导致panic。
使用技巧
- 通道所有者需要考虑的问题
- 通道所有者,会发生一些事情:因为我们是初始化频道的人,所以我们要了解写入nil通道会带来死锁的风险
- 因为我们是初始化频道的人,所以我们要了解关闭nil通道会带来恐慌的风险。
- 因为我们是决定频道何时关闭的人,所以我们要了解写入已关闭的通道会带来恐慌的风险。
- 因为我们是决定何时关闭频道的人,所以我们要了解多次关闭通道会带来恐慌的风险。
- 我们在编译时使用类型检查器来防止对通道进行不正确的写入。
- 通道的消费者,我只需要担心两件事情:
- 通道什么时候会被关闭:检查读取操作的第二个返回值就可以
- 处理基于任何原因出现的阻塞
- 在自己的程序中尽可能做到保持通道覆盖范围最小,即channel作用域最小