首先,未初始化的channel变量值为nil: channel底层其实就是个指针,这个下面会讲,所以其nil值,在底层就是用0表示的,如上面的输出。 上图是main函数的汇编,其中选中的两行,就是调用p函数的逻辑。 p函数的参数是通过ax寄存器传递的,由上图可见,在调用p函数之前,ax寄存器的值,通过xorl指令进行了清零,这也是channel变量的nil值,在底层是用0表示的另一个佐证。 channel是通过make函数创建的,make可以创建unbufferedchannel,也可以创建bufferedchannel: make其实并不是一个真正的函数,它会在编译阶段,被go编译器替换为对runtime。makechan函数的调用: 该替换信息也可以通过汇编查看: 上图中选中行为make函数的汇编逻辑,其中hello。go:8的第四行,和hello。go:11的第三行,都是在调用runtime。makechan函数。 在看runtime。makechan函数之前,我们先通过上图的汇编,看下make创建unbufferedchannel和bufferedchannel的方式有什么不同。 源码中第8行是创建unbufferedchannel,对应到汇编里,就是上图中hello。go:8的那4行。 源码中第11行是创建bufferedchannel,对应到汇编里,就是上图中hello。go:11的那4行 由上图可见,hello。go:8和hello。go:11的逻辑基本相同,都是先将要创建channel的类型,放到ax寄存器里,然后将要创建channel的buffersize,放到bx寄存器里,最后调用runtime。makechan函数,转入真正的创建channel逻辑。 这里需要说明一下,go函数之间的调用,其参数和结果的传递,主要是通过寄存器来完成的(go1。17之后,之前是通过栈),对于上面的makechan函数来说,第一个参数是通过ax寄存器传递的,第二个参数是通过bx寄存器传递的。 有关go函数之间调用,参数和结果传递方式的具体规则,请参考以下文档: https:github。comgolanggoblobmastersrccmdcompileabiinternal。md 再回到上面的问题,由上图汇编可见,创建unbufferedchannel和bufferedchannel的流程是一样的,唯一区别就是bx寄存器的值不同,即指定的buffersize不一样。 hello。go:8是创建unbufferedchannel,bx的值是0,hello。go:11是创建bufferedchannel,bx的值是8,即我们在源码中指定的值。 所以,make(chanint)其实等价于make(chanint,0),即buffersize等于0。 接下来我们看一下runtime。makechan函数的实现: 连接上文,参数t,即channel的类型,是通过ax寄存器传递的,参数size,即channel的buffersize,是通过bx寄存器传递的。 上面我们也提到,channel变量底层其实就是个指针,该指针的类型,就是上图中makechan函数的返回类型hchan: hchan结构体各字段的用途是: qcount表示的是当前channel的buffer里已经缓冲了几个元素。如果是unbufferedchannel,该字段一直为0。 dataqsiz表示的是当前channel的buffer里最多可缓冲几个元素,即上文我们提到的buffersize。如果是unbufferedchannel,该字段一直为0。 buf指向的是用于接受缓冲元素的总内存,我们可以把它理解成一个数组,数组的元素类型就是channel的元素类型,数组的最大容量,就是上面dataqsiz的值。如果是unbufferedchannel,该buf不用分配。 elemsize表示的是channel元素类型的大小。 closed表示的是当前channel是否已关闭,即调用过close(c)方法。 elemtype表示的是channel的元素类型。 sendx表示的是下一次向channel中发送数据,该数据会被拷贝到buf字段表示的元素数组的位置,当该位置超过数组最大值以后,会从0重新开始。如果是unbufferedchannel,该字段一直为0。 recvx表示的是下一次从channel中接收数据,该接收会从buf字段表示数组的recvx位置拷贝数据到目标内存。如果是unbufferedchannel,该字段一直为0。 recvq表示的是等待从channel中接受数据的goroutine队列。当buf中缓冲的元素个数为0,且sendq表示的等待发送数据的goroutine队列为空时,再有goroutine想从这个channel中读取数据,就会被阻塞等待在这里队列里。 sendq表示的是等待向channel中发送数据的goroutine队列。当buf中缓冲的元素个数已达到最大值,且recvq表示的等待接受数据的goroutine队列为空时,再有goroutine想向这个channel里发送数据,就会被阻塞等待在这个队列里。 lock表示的是channel的锁,为了保证channel读写的并发安全,channel的很多操作都是加锁的。 有关以上字段是如何配合使用的,这个会在下文的各种示例分析中看到。 这里有一点需要注意的是,因为channel的元素传递是拷贝操作,所以如果channel元素类型占较大内存,要考虑是否应该传递其指针。 上面makechan的那张图中,83到85行是调试信息,如果debugChan为true,则在创建channel时,会输出channel的元素大小,及可缓冲的元素个数等信息。 下面我们修改下debugChan的值,然后写个例子看下其输出。 示例代码: gobuild构建程序并执行: 上图选中行中,第一行是make(chanS)的输出,该make创建的channel,元素大小为80字节,可缓冲元素个数为0。 第二行是make(chanS,8)的输出,它创建的channel,元素大小也为80字节,可缓冲元素个数为8。 因为channel元素传递是拷贝操作,所以对于这个示例来说,每次send或receive时,都要拷贝80字节,此种情况下,就应该考虑将该channel改为传递S的指针,而不是S本身。 接下来看下如何向channel中发送数据: 也是在编译阶段,cv被转成了对runtime。chansend1函数的调用: 同样,我们也可以根据汇编代码得出该信息: 上图是send函数的汇编逻辑,其中和发送相关的,是上图中的选中行,即hello。go:10的那4行。 根据go的callingconvention可知,上面示例中的send函数在被调用时,参数c被放到了ax寄存器里,参数v被放到了bx寄存器里。 再看上图中的汇编代码,hello。go:10中的第一行把bx的值,放到了栈的0x10(SP)位置,接着把该位置的地址,又放到了bx里,也就是说,此时bx里存放的是参数v的地址。 接着在hello。go:10中的第四行,调用runtime。chansend1函数,该函数的两个参数,代表channel的变量c,以及要发送的值v的地址,同样也是通过ax和bx传递过去。 来看下runtime。chansend1函数: 该函数的参数c,就是上面示例中send函数的参数c,该函数的参数elem,就是上面示例中send函数参数v的地址。 该函数又调用了chansend函数: chansend函数是向channel发送数据的主体逻辑,其大致步骤请参考上图中的注释,同时也可以结合上文提到的,hchan结构体中各字段的意义,来理解这段代码。 由上图可见,为了保证逻辑的正确性,向channel发送数据的操作都进行了加锁,所以,虽然channel面向用户来说是无锁的,但其内部实现是依靠锁来完成的。 再来看下从channel中接收数据: 在编译阶段,v:c被转换成了对函数runtime。chanrecv1的调用: 对照汇编进一步确认: 上图是receive函数的汇编逻辑,当该函数被调用时,ax寄存器里的值是receive函数的参数c,即channel变量。 上图选中行,是v:c的汇编代码,它先将0x10(SP)开始的8字节内存清零,然后再将该内存的地址赋值给bx,最后调用runtime。chanrecv1。 由此我们可以推测,runtime。chanrecv1函数应该有两个参数,一个是channel变量,另一个是内存地址,用于存放要接收到的数据。 看下runtime。chanrecv1: 参数类型与个数和我们推测的一样,它又调用了chanrecv: chanrecv是从channel中接收数据的主体逻辑,其大致步骤请参考上图中的注释,同时也可以结合上文提到的,hchan结构体中各字段的意义,来理解这段代码。 和chansend类似,chanrecv的主体逻辑也是在加锁下完成的。 以上就是channel的创建,发送数据,接受数据等主要操作的实现,了解这些实现,就算是对channel有一个比较好的理解了。 但除此之外,channel还有一些细节知识,需要我们注意。 1。向nilchannel发送数据会永久阻塞 上图示例中是在向nilchannel发送数据,但似乎没成功,并不像之前说的,向nilchannel发送数据会永久阻塞。 其实,这个错误是go内部检查死锁的机制,它并不是由向nilchannel发送数据引起的。 比如,下面的写法也会报这个错: 上图示例中创建了一个unbufferedchannel,然后向其发送数据,也报错了,因为这种写法会自己阻塞自己。 那如何不报这个错,然后可以看到,向nilchannel发送数据会永久阻塞呢? 看下面这个例子: 再开一个goroutine就好了,在这个示例中,c8会一直阻塞,没有返回。 向nilchannel发送数据会永久阻塞,对应的底层实现为: 2。向closedchannel发送数据会发生runtimepanic 对应的底层实现为: 3。从nilchannel中接收数据会永久阻塞 对应的底层实现为: 4。从closedchannel中接收数据,会返回channel元素类型的zerovalue 对应的底层实现为: 从上图中还可以得出一个结论,就是即使channel被关闭了,如果channelbuffer中有数据,还是会正常返回数据。 5。从channel中接收数据可以有两个返回值,第二个返回值可近似表示channel是否已关闭 该示例main函数的汇编代码: 由上面的选中行可知,编译器将v,ok:c转成了对runtime。chanrecv2函数的调用: chanrecv2除了将从channel中接收的数据,拷贝到elem指针指向的内存外,还返回了一个received布尔值。 当channel被关闭后,且其buffer中没有数据,再从channel中接收数据,chanrecv2返回的received值就为false,表示channel已经被关闭了。 6。对channel的len和cap操作是无锁的 其main函数的汇编为: 上图中第13行汇编MOVQ0(AX),CX表示的是示例中的len(c)操作,第21行汇编MOVQ0x8(CX),CX表示的是示例中的cap(c)操作。 由上图可见,对channel的len和cap操作,在汇编层面都是一条mov指令,并不像之前的,比如对channel的接收操作,是转换成对runtime。chanrecv1函数的调用,且在该函数中,有加锁解锁操作。 综上可知,对channel的len和cap操作是无锁的。 那为什么这两条mov指令,就可以获得channel的len和cap值呢? 首先看上图汇编,13行中的ax和21行中的cx,存放的都是新建channel结构体的地址。 那这两条mov指令的意思是: MOVQ0(AX),CX将channel结构体偏移量为0位置上的8字节放入cx中 MOVQ0x8(CX),CX将channel结构体偏移量为8位置上的8字节放入cx中 再看下channel结构体的定义: 该结构体偏移量为0位置上的8字节,就是qcount,即当前channelbuffer中已经缓冲的元素个数,也就是len(c)。 偏移量为8位置上的8字节,就是dataqsiz,即当前channelbuffer中最大能缓冲的元素个数,也就是cap(c)。 7。close一个receiveonlychannel会编译时报错 对应的编译器实现: 8。close一个nilchannel或closedchannel会发生runtimepanic 对应的底层实现为: 9。channel的forrange形式 forrange形式其实是语法糖,在编译阶段,其会被转换成类似上图注释那样的for循环。 对应的编译器转换代码为: 我们也可以根据汇编,看forrange转化后的样子: 由上图可见,在汇编层面,其一直在调用runtime。chanrecv2函数,这个函数上面我们也提到过,就是对应于v,ok:c操作。 channel的select形式在go内的实现比较复杂,我们来分步讲下。 10。空select语句永久阻塞 对应的底层实现为: 上图中的block对应于runtime。block函数: 即永久阻塞。 11。只有一个case的情况,会直接转换成对channel的操作,等价于没有select部分: 通过上图的汇编可见,示例中操作1和2是等价的。 编译器中对一个case的转换代码为: 12。有两个case,且其中一个是default,会转换成对channel的非阻塞操作,如果没成功,则会执行default语句: 上图中的runtime。selectnbsend就是c1的非阻塞版,其源码为: selectnbsend函数内会调用chansend,该函数正是我们之前说的,向channel发送数据的函数,其中false参数表示该发送是非阻塞的。 上图中的selectnbrecv也是非阻塞的从channel中接收数据,对应于select只有两个case,其中一个case是vc,另外一个是default的情况。 编译器对该类情况的转换代码为: 13。其他情况就是select的通用形式了,编译器会把select语句转换成对runtime。selectgo函数的调用: 对应的汇编代码: 对应的编译器转换代码: selectgo函数会在各个已经就绪的channel里,随机选择一个执行,因为逻辑非常多,这里就展示下该函数的代码位置,有兴趣的可以自己看下: 有关selectgo是随机选择就绪channel的,我们可以写个测试验证下: 看到没,两个值基本相同。 好了,以上就是各种channel操作对应的底层实现,希望通过此篇文章,能让大家对golang中的channel有更好的了解。