Go 语言基础 - 通道(channel)
Go 采用了 CSP 的并发模型。它的基础是 channel 和 goroutine。channel 是 Go 语言的一种数据类型,是传递消息的载体,使用起来十分简单,是并发安全的;而 goroutine 是并发执行的实体。
本文为以后讲「Go的并发模型」打个基础,讲解 channel 是如何使用的。
基本使用
我们可以将通道认为是队列(先进先出),队列的长度就是通道容纳消息的容量。
消息的类型可以是任意的,它需要在我们初始化或声明通道变量的时候指定。
通道的关键字是chan
,通道变量的类型是chan <type>
,属于 引用类型,
我们可以通过下面的方式得到通道变量:
1 | // 声明通道,chan0是一个int类型的通道,初始值为nil |
我们使用操作符<-
对通道进行操作,它可以放在通道的左边或右边,分别代表从通道获取消息
和将消息放入通道
。下面是具体例子:
1 | chan1 := make(chan int, 5) |
对于已经不再使用的通道,我们要养成主动关闭的习惯,接收方可以通过关闭的动作感知到通道的状态,而做出处理。
关闭通道使用close
方法:
1 | close(chan1) |
对于通道的容量,有下面的特性:
- 当通道的容量已满时,发送操作会被阻塞,直到有空位为止;
- 当通道为空时,接收操作会被阻塞,直到有新的元素到来。
通道特性
我们现在知道了通道是如何使用的,包括声明,赋值取值和关闭。
通道存放的消息,其实都是元素的副本或者是元素引用的副本(浅复制),而不是原始元素。所以在元素进入通道时,通道是做了「复制」操作的。
还记得我们在最开始的时候说过,通道是「并发安全」的,使用简单,是因为通道本身的特性为我们做了保证:
- 阻塞,对于发送操作和接收操作,在操作没有完成之前,代码会被阻塞,不会进行后续操作;
- 同一个通道的发送操作是互斥的,接收操作也是互斥的,原因是为了保证顺序的一致,先到达的操作肯定会先完成;
- 单个操作是原子性的。前面提到,元素进出通道时会触发复制操作,以接收操作为例,操作包含了赋值给新变量、删除通道对应的元素,这两步形成的操作是原子性的。
通道的特性保证了它的简单性,使用者可以省去许多开发上的精力。
非缓冲通道
当我们使用make创建一个通道时,不指定容量,或者容量为0,那么该通道是不带缓冲的。
对于非缓冲通道的使用,它的接收操作和发送操作,执行时便会阻塞,只有两者同时存在时,操作才会进行。也就是说,收和发必须同时存在,否则便会阻塞。
单向通道
通道可以是单向的,在声明或初始化通道时,我们可以指定通道是send-only
还是receievd-only
,只需要在关键字chan
的左边或右边加上<-
即可。
1 | // chan1 是一个send-only通道,只进不出 |
单向通道可以起到约束作用,比如作为方法的参数,它可以限制方法对通道的使用。
比如,下面的方法,它限制了变量 ch 只能接收,不能发送1
2
3func receivedOnlyChannel(ch <-chan int) {
fmt.Println(<-ch)
}
然后我们可以向 receivedOnlyChannel 传入通道变量。
1 | chan3 := make(chan int, 5) |
注意Panic
如果对通道使用不当,会造成panic发生。
- 当通道关闭后,再次对通道进行关闭,会引发panic;
- 当通道关闭后,对其进行发送操作,会引发panic;
- 当我们只有一个goroutine,而通道发生了阻塞,会引发panic:
all goroutines are asleep
,程序死锁。
使用range获取通道元素
像我们在遍历其他容器一样,我们可以使用关键字range
,对通道的元素进行顺序接收:
1 | chan1 := make(chan int, 5) |
我们需要特别注意第5行的close
操作,如果我们只有一个goroutine
时,不关闭chan1
将会造成for
循环的阻塞,导致panic
发生。
select语句
针对通道的接收,Go 语言提供了select
语句,它的使用类似于switch
语句,不同的是,它接收的是通道参数。先看下面一个例子:
1 | chan1 := make(chan int, 1) |
我们定义了3个通道:chan1、chan2、chan3,其中只有chan2是有元素的,chan1没有元素存在,而chan3只是声明而没有初始化,是个nil值。
对于select
来说,它会选择一个满足条件的分支进行执行,分支的case
表达式是顺序求值的,那么会出现下面的情况:
- 对所有
case
表达式按顺序执行求值操作,如果当前case
是阻塞的,则认为是不满足条件; - 假设只有一个
case
满足条件,则执行当前case
分支,如果不止一个case
表达式满足条件,则采用伪随机算法选择其中一个执行; - 当所有
case
表达式都不满足条件时:- 如果存在
default
分支,则执行default
分支; - 否则,
select
语句被阻塞,直到有任意一个分支满足为止。
- 如果存在
当我们需要对通道不断进行获取时,可以将
for
语句与select
语句搭配使用,但需要注意的是,我们需要通过接收通道时的第二个参数,主动感知通道是否已关闭,来做出相应的动作,让我们的程序逻辑更加合理。
总结
本文是对 Go 语言通道 - Channel 的使用说明,包括如何初始化和使用、通道的特性、使用注意事项,以及如何搭配range
和select
使用,其中的 demo 可以在 此处 获得。
如果有错误或补充,欢迎留言指正。
- 本文链接:https://keepmoving.ren/golang/channel/
- 版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 CN 许可协议。转载请注明出处!