1. channel 是否线程安全?锁用在什么地方?#
分析
channel配合goroutine可以用来实现并发编程,并且是go语言推荐的并发编程模式,那么肯定是可以保证线程安全的,可以先回顾下channel的底层定义,channel用make函数创建初始化的时候会在堆上分配一个runtime.hchan类型的数据结构
type hchan struct {
qcount uint // 循环队列中的元素总数
dataqsiz uint // 循环队列大小
buf unsafe.Pointer // 指向循环队列的指针
elemsize uint16 // 循环队列中的每个元素的大小
closed uint32 // 标记位,标记channel是否关闭
elemtype *_type // 循环队列中的元素类型
sendx uint // 已发送元素在循环队列中的索引位置
recvx uint // 已接收元素在循环队列中的索引位置
recvq waitq // 等待从channel接收消息的sudog队列
sendq waitq // 等待向channel写入消息的sudog队列
lock mutex // 互斥锁,对channel的数据读写操作加锁,保证并发安全
}
可以看到channel的底层实现中是有锁的,是通过mutex来保证线程安全的,所以在回答的时候要突出底层实现有锁
回答
一般来说,我们对channel就只有读、写、关闭三种操作,这三种操作,channel底层数据结构都用同一把runtime.Mutex来进行保护
2. channel 的底层实现原理(数据结构)#
分析
这个问题其实是上一个问题的补充,channel的底层实现是一个hchan的结构,hchan的结构定义回顾这个图

type hchan struct {
qcount uint // channel 环形数组中的素数量
dataqsiz uint // channel 环形数组的容量
buf unsafe.Pointer // 指向Channel 环形数组的一个指针
elemsize uint16 // 元素类型的字节数
closed uint32 // 是否关闭
elemtype *_type // 元素类型
sendx uint // send index 下一次的位置
recvx uint // receive index 下一次的位置
recvq waitq // list of recv waiters 接等待队列
sendq waitq // list of send waiters 等待队列
lock mutex // runtime.mutex, 保证channel并发安全
}
回答
对于包含缓冲的channel,go语言的channel底层是一个hchan的结构,里面包含一个指向循环数组的指针,这个循环数组是用于存储数据的,当然还包含下次读取和下次发送的数据最引位置recvx和sendx
还包含两个goroutine等待队列,在一个goroutine对这个channel读写阻塞的时候会分流放到这两个队列里,发送数据阻塞就放到sendq这个等待队列,接收数据阻塞就放到recvq这个等待队列
为了保证channel的线程安全,hchan结构还有一个互斥锁,用作数据读写时候加锁,当然close channel也会用到这个互斥锁
3. nil、关闭的channel、有数据的channel,再进行读、写、关闭会怎么样?(各类变种题型)#
分析
主要是考察对channel在各个状态下进行读写操作会出现什么结果,这块建议自己代码跑一下各个场景,加深一下理解
回答
| 操作 \ 状态 | 未初始化(nil) | 关闭(closed) | 正常(normal) |
|---|---|---|---|
| 关闭 | panic | panic | 正常关闭 |
| 写 | 当前goroutine永久性挂起,可能导致死锁 | panic | 阻塞挂起或者成功发送 |
| 读 | 当前goroutine永久性挂起,可能导致死锁 | 读取缓冲区数据,读完后,每次读取都返回零值 | 阻塞挂起或者成功接收 |
对nil的channel进行读和写 都会造成当前goroutine永久阻塞(如果当前goroutine是main goroutine,则会让整个程序直接报fatal error 退出,也就是报错deadlock),关闭则会发生panic
对已经关闭的channel进行写 和 再次关闭,都会导致panic,而读操作的话,会一直将channel中的数据读完,读完之后,每次读channel都会获得一个对应类型的零值
对一个正常的channel进行读写有两种情况
a. 读:阻塞挂起或者成功发送
b. 写:阻塞挂起或者成功接收
c. 关闭:正常close
4. 对channel 进行读写数据的流程是怎样的#
分析
考察对channel 底层结构以及chansend和chanrecv流程的掌握程度,下面回答不区分有缓冲channel 和 无缓冲channel,注意理解
下面是对一个非nil,且未关闭的channel进行读写的流程
回答
操作一个不为nil,并且未关闭的channel,读和写都有两种情况
读操作:
a. 成功读取:
如果channel中有数据,直接从channel里面读取,并且此时如果写等待队列里面有goroutine,还需要将队列头部goroutine数据放入到channel中,并唤醒这个goroutine
如果channel没有数据,就尝试从 写等待队列 头部goroutine读取数据,并做对应的唤醒操作
b. 阻塞挂起: channel里面没有数据 并且 写等待队列为空,则将当前goroutine 加入 读等待队列中,并挂起,等待唤醒
写操作
a. 成功写入:
如果channel 读等待队列不为空,则取 头部goroutine,将数据直接复制给这个头部goroutine,并将其唤醒,流程结束
否则就会尝试将数据写入到channel 环形缓冲中
b. 阻塞挂起: 通道里面无法存放数据 并且 读等待队列为空,则当前goroutine 加入 写等待队列中,并挂起,等待唤醒
5. select的底层原理#
分析
select也被称为多路select,指的是一个goroutine 可以服务多个 channel的读或写操作,要清楚的知道 select分为两种,包含非阻塞型select(包含default分支的) 和 阻塞型select(不包含default分支的),然后再回答其对应原理
回答
select的核心原理是,按照随机的顺序执行case,直到某个case完成操作,如果所有case的都没有完成操作,则看有没有default分支,如果有default分支,则直接走default,防止阻塞
如果没有的话,需要将当前goroutine 加入到所有case对应channel的等待队列中,并挂起当前goroutine,等待唤醒。
如果当前goroutine被某一个case 上的channel操作唤醒后,还需要将当前goroutine从所有case对应channel的等待队列中剔除