目录

 


概念

go语言提倡“以通信作为手段来共享内存”,而channel就是最直接和最重要的体现。channel指通道类型,是Go语言预定义的数据类型之一,使用chan表示通道类型的关键字。
channel提供了一种机制:可以同步两个被并发执行的函数,也可以让两个函数通过相互传递特定类型的值进行通信。

缓冲与非缓冲

先从初始化看两种通道的区别:缓冲:make(chan T,10),非缓冲:make(chan T)。此处可以引申两个概念:长度len和容量cap。len指现在通道中包含的元素;cap指初始化时设置的值,该值之后不会被改变,可以通过该值判断是否带有缓冲,容量为0,那么是不带缓冲的通道。

非缓冲通道同步的传递我们发送给它的元素值,而缓冲通道是异步的。
针对非缓冲,发送操作在向通道发送元素值的时候,会等待能够接收该元素值的那个接收操作,并且只有被成功接收,才会真正的完成执行;针对缓冲,由于元素值的传递是异步的,所以发送操作在成功向通道发送元素值之后就会立即结束,不关心是否有接收操作要接收;
针对非缓冲,接收操作会在相应的发送操作完成之前完成,而这对于缓冲通道恰恰相反;
针对非缓冲,只有存在可配对的操作的时候,传递元素值的动作才可能真正开始。

双向与单向

先从表示看两种通道的区别:双向通道:chan T,单向通道:chan <- T(只能发送的通道),<-chan T(只能接收的通道)。
注意:<- chan T(v)(将变量v的类型转为chan T,然后再从该值中接收一个元素),(<-chan T)(v)(将变量v的类型转换为一个接收通道类型)

由上述表示可以看出,单向通道要么只能发不能收,要么只能收不能发,其真正意义是在一定作用域(某个代码块)内约束使用者对它的使用方式,也即暗示和控制相应数据的流转方向,一般不应该出现在一个变量的声明中,这样是没有意义的;而双向通道既可以发送也可以接收。

for

for语句及其range子句可以持续地从一个通道中接收元素值,但是range子句的迭代目标不能是一个发送通道。如果range一个发送通道,会报类似的错误:invalid operation: range a (receive from send-only type chan<- int);如果遍历一个未初始化的通道或通道中没有元素,那么会阻塞。

在需要循环的接收通道中的元素值的场景下,优先使用for来实现。for会不断地尝试从通道中接收元素值,直到该通道被关闭。在相关的通道被关闭后,若通道中已无元素值或当前的Goroutine正阻塞于此,则这条for语句的执行回立即结束;若当此时的通道中还有遗留的元素值时,运行时系统会等for将它们全部接收后再结束该语句的执行。

select

select语句仅能被用于发送和接收通道中的元素值。一个select语句在被执行的时候会选择执行其中的一个某一个分支,具体分支选择规则如下:
1.对于每一个case表达式,都至少会包含一个代表发送操作的发送表达式或一个代表接收操作的接收表达式,同时也可能包含其他的表达式。当case表达式被求值时,会按照从左往右的顺序被求值。
2.case分支表达式都会在该语句执行开始时先被求值,求值的顺序是依从代码编写的顺序从上到下的。
3.对于每一个case表达式,如果其中的发送或接收表达式在被求值时,相应的操作正处于阻塞状态,那么对该case表达式的求值就是不成功的。
4.仅当select语句中的所有case表达式都被求值完毕后,它才会开始选择候选分支。
5.如果select语句发现同时有多个候选分支满足条件,那么它就会用一种伪随机的算法在这些分支中选择一个并执行。
6.一条select语句只能有一个default分支,并且只有在case均不满足才会执行,与顺序无关。并且如果有default,那么无论涉及通道操作的表达式是否有阻塞,select语句都不会被阻塞;如果没有加入默认分支,一旦所有的case都没有满足要求,就会被阻塞,直到至少有一个case表达式满足条件为止。
7.select语句的每次执行,包括case表达式求值和分支选择都是独立的。至于是否并发安全,就要看分支中是否包含并发不安全的代码了。

此外,select语句只能对其中的每一个case表达式各求值一次。所以,如果我们想连续或定时地操作其中的通道后,就需要借助for。

Q & A

1.通道的发送和接收操作都有哪些基本的特性?
1).对于同一个通道,发送操作之间都是互斥的,接收操作之间也是互斥的。注意,元素值从外界进入通道时会被复制。更具体说,进入通道的并不是在接收操作符右边的那个元素值,而是副本。元素值从通道进入外界时会被移动。这个移动操作实际上包含了两步,第一步是生成正在通道中的这个元素值的副本,并准备给到接收方,第二步是删除在通道中的这个元素值。
2).发送操作和接收操作中对元素值的处理都是不可分割的。不可分割意思的不会被打断。发送操作要么还没复制元素值,要么已经复制完毕,绝不会出现只复制了一部分的情况;接收操作在准备好元素值的副本之后,一定会删除掉通道中的原值,绝不会出现通道中仍有残留的情况。
3).发送操作在完全完成之前会被阻塞,接收操作也是如此。发送操作包括复制元素值和放置副本到通道内部两个步骤。这两个步骤完全完成之前,发起这个发送操作的那句代码会一直阻塞在那里,以后的代码不会有执行的机会,直到这句代码的阻塞解除;接收操作包括复制通道内的元素值,放置副本到接收方,删除原值三个步骤。

2.发送操作和接收操作什么时候会引发panic?
1).通道关闭,继续发送操作,引发panic:send on closed channel
2).通道关闭,继续接收操作,不会panic,除非再次关闭通道,引发panic:close of closed channel.注意接收操作是可以感知到通道的关闭,并能够安全退出,elem2,ok:=<-ch1
如果ok=false,则表明通道关闭,但是有可能有延时。
因此,在发送端调用close以关闭通道不会对接收端接收该通道中已有的元素值产生任何影响;无论怎样都不应该在接收端关闭通道,因为无法判断发送端是否还会向该通道发送元素值。

3.close关闭是否会立即被关闭?
通道被关闭后不会立即将false作为相应接收操作的第二个结果,而是会等到接收端把已在通道中的所有元素值都接收到之后才这样做。

4.发送操作和接收操作在什么时候可能被长时间的阻塞?
1).针对缓冲通道,如果通道已满,所有的发送操作都会被阻塞,直到通道中有元素值被接收走;如果通道已空,那么对它的所有接收操作都被阻塞,直到通道中有新的元素值出现。其以非同步方式传递数据,元素值从先从发送方复制到缓冲通道,之后再由缓冲通道复制给接收方。
针对非缓冲通道,一开始执行就会被阻塞,直到配对的操作开始执行,才会继续传递。其以同步方式传递数据。并且数据直接从发送方复制到接收方的,中间并不会用非缓冲通道做中转。
2).值为nil或未被初始化的通道,无论具体类型是啥,发送操作和接收操作都会永久处于阻塞状态。当只声明该类型的变量但没有用make函数对它进行初始化,该值为nil.

5.如果在select语句中发现某个通道已关闭,那么应该怎样屏蔽掉它所在的分支?
发现某个channel被关闭后,为了防止再次进入这个分支,可以把这个channel重新赋值成为一个长度为0的非缓冲通道,这样这个case就一直被阻塞了,示意代码如下:
for {
select {
case _, ok := <-ch1:
if !ok {
ch1 = make(chan int)
}
case ..... :

default:
    
}
}

6.在select语句与for语句联用时,怎么直接退出外层的for语句?
可以用break和标签配合使用,直接break出指定的循环体,或者goto语句直接跳转到指定标签执行

参考资料