文章目录
1 概念
channel 是一个通道,用于端到端的数据传输,这有点像我们平常使用的消息队列,只不过 channel 的发送方和接受方是 goroutine 对象,属于内存级别的通信。
2 分类
3 操作
在深入了解 channel 的底层之前,我们先来看看 channel 的常用用法。
3.1 channel 的创建
3.1.1 无缓冲channel
ch := make(chan int)
对于无缓冲的 channel,一旦有 goroutine 往 channel 写入数据,那么当前的 goroutine 会被阻塞住,直到有其他的 goroutine 消费了 channel 里的数据,才能继续运行写入数据。
3.1.1 带缓冲channel
还有另外一种是有缓冲的 channel,它的创建是这样的:
ch := make(chan int, 10)
其中,第二个参数表示 channel 可缓冲数据的容量。只要当前 channel 里的元素总数不大于这个可缓冲容量,则当前的 goroutine 就不会被阻塞住。
另外,我们也可以声明一个 nil 的 channel,只是创建这样的 channel 没有意义,读、写 channel 都将会被阻塞住。一般 nil channel 用在 select 上,让 select 不再从这个 channel 里读取数据,如下用法:
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
if !ok { // 某些原因,设置 ch1 为 nil
ch1 = nil
}
}()
for {
select {
case <-ch1: // 当 ch1 被设置为 nil 后,将不会到达此分支了。
doSomething1()
case <-ch2:
doSomething2()
}
}
3.2 channel的读写
ch := make(chan int,10)
ch<-1 //向管道中写入数据
readdata:=<-ch //从管道中读取数据
3.3 channel的关闭(****)
ch := make(chan int,10)
...
close(ch)
面试题1:当关闭channel之和再操作channel会发生什么?
解:
写数据:则程序会直接 panic 退出;
读数据:(1) 有数据:读到关闭之前写入的数据;
(2) 无数据:将得到零值(无效数据),即对应类型的默认值。
面试题2:对关闭的chan进行读操作,如何保证读取的数据是有效的而非无效零值呢?
解:
使用_,ok:=<-chan 判断是否关闭:
if v, ok := <- ch; ok {
fmt.Println(v) //ok为true表示读取管道成功,数据是有效的
}
3.4 channel 和 select
在写程序时,有时并不单单只会和一个 goroutine 通信,当我们要进行多 goroutine 通信时,则会使用 select 写法来管理多个 channel 的通信数据:
ch1 := make(chan struct{})
ch2 := make(chan struct{})
// ch1, ch2 发送数据
go sendCh1(ch1)
go sendCh1(ch2)
// channel 数据接受处理
for {
select {
case <-ch1:
doSomething1()
case <-ch2:
doSomething2()
}
}
3.5 channel 的死锁
前面提到过,往 channel 里读写数据时是有可能被阻塞住的,一旦被阻塞,则需要其他的 goroutine 执行对应的读写操作,才能解除阻塞状态。
然而,阻塞后一直没能发生调度行为,没有可用的 goroutine 可执行,则会一直卡在这个地方,程序就失去执行意义了。此时 Go 就会报 deadlock 错误,如下代码:
package main
func main() {
ch := make(chan int)
ch<-10
}
运行结果:
fatal error: all goroutines are asleep - deadlock!
因此,在使用 channel 时要注意 goroutine 的有发有取,避免 goroutine 死锁!
4 channel 底层原理(*****)
前面提及过 channel 创建后返回了 hchan 结构体,现在我们来研究下这个结构体,它的主要字段如下:
type hchan struct {
//channel分为无缓冲和有缓冲两种。
//对于有缓冲的channel存储数据,借助的是如下循环队列的结构
qcount uint // 循环队列中的元素数量
dataqsiz uint // 循环队列的长度
buf unsafe.Pointer // 指向底层循环队列的指针
elemsize uint16 //能够收发元素的大小
closed uint32 //channel是否关闭的标志
elemtype *_type //channel中的元素类型
//有缓冲channel内的缓冲数组会被作为一个“环型”来使用。
//当下标超过数组容量后会回到第一个位置,所以需要有两个字段记录当前读和写的下标位置
sendx uint // 下一次发送数据的下标位置
recvx uint // 下一次读取数据的下标位置
//当循环数组中没有数据时,收到了接收请求,那么接收数据的变量地址将会写入读等待队列
//当循环数组中数据已满时,收到了发送请求,那么发送数据的变量地址将写入写等待队列
recvq waitq // 读等待队列
sendq waitq // 写等待队列
lock mutex //互斥锁,保证读写channel时不存在并发竞争问题
}