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赋值为nil,或者定义后不调用make进行初始化。按照Go语言的语言规范,读写空通道是永远阻塞的。其实在函数runtime.chansend和runtime.chanrecv开头就有判断这类情况,如果发现参数c是空的,则直接将当前的goroutine放到等待队列,状态设置为waiting。
对已关闭的chan读写
对未初始化的chan读写:读写未初始化的 chan 都会阻塞
chan
此时是等于 nil
,当它不能阻塞的情况下,直接返回 false
,表示写 chan
失败chan
能阻塞的情况下,则直接阻塞 gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
, 然后调用 throw(s string)
抛出错误,其中 waitReasonChanSendNilChan
就是刚刚提到的报错 "chan send (nil chan)"