2026-06-07 11:08:10 +08:00
|
|
|
|
---
|
2026-06-07 12:14:39 +08:00
|
|
|
|
tags: [go, golang, interview, channel-questions]
|
|
|
|
|
|
create time: 2026-06-07 14:30
|
2026-06-07 11:08:10 +08:00
|
|
|
|
---
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
# Channel 面试题 📡
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
## 概述
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
本文件涵盖 Go Channel 的 11 道高频面试题,涉及 CSP 模型、底层原理、收发流程、select 机制等核心考点。Channel 是 Go 并发编程的灵魂,理解 Channel 是掌握 Go 并发的关键。
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
## 关联笔记
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
- [[hzh/GolangStar/Go语言进阶/Channel]] — Channel 详细讲解
|
|
|
|
|
|
- [[hzh/GolangStar/Go语言进阶/Select]] — select 多路复用详解
|
|
|
|
|
|
- [[hzh/GolangStar/Go语言进阶/Context]] — Context + Channel 结合使用
|
|
|
|
|
|
- [[hzh/GolangStar/Go语言原理/channel原理]] — hchan 源码级分析
|
|
|
|
|
|
- [[hzh/GolangStar/Go面试题库/内存管理面试题]] — goroutine/channel 泄漏
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
## 正文
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
### Q1:什么是 CSP 模型? 🟢简单
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
> [!question] ❓ 思考一下
|
|
|
|
|
|
> "不要通过共享内存来通信,而要通过通信来共享内存"——这句话怎么理解?
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
## 参考答案
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
CSP(Communicating Sequential Processes)并发编程模型的核心思想:**通过通信共享内存,而不是通过共享内存来通信。**
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
| 特点 | 说明 |
|
|
|
|
|
|
|------|------|
|
|
|
|
|
|
| 避免共享内存 | Goroutine 不直接修改变量,而是通过 Channel 传递数据 |
|
|
|
|
|
|
| 天然同步 | Channel 的 send/recv 自带同步,无需手动加锁 |
|
|
|
|
|
|
| 易于组合 | Channel 可以嵌套构建复杂模式(管道、超时控制) |
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
> [!tip] 💡 面试技巧
|
|
|
|
|
|
> 对比 Java 的线程通信方式(wait/notify + synchronized),强调 Go 用 Channel 统一了"数据传递"和"同步"两个问题,设计更优雅。
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
---
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
### Q2:Channel 的底层实现原理? 🟡中等
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
## 参考答案
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
Channel 的底层是 `hchan` 结构体,包含三大组件:
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
|
|
|
|
|
```go
|
2026-06-07 12:14:39 +08:00
|
|
|
|
type hchan struct {
|
|
|
|
|
|
qcount uint // 队列中元素数量
|
|
|
|
|
|
dataqsiz uint // 环形缓冲区大小
|
|
|
|
|
|
buf unsafe.Pointer // 指向缓冲区的指针(仅 buffered channel)
|
|
|
|
|
|
elemsize uint16 // 元素大小
|
|
|
|
|
|
closed uint32 // 是否关闭
|
|
|
|
|
|
elemtype *_type // 元素类型
|
|
|
|
|
|
sendx uint // 发送索引
|
|
|
|
|
|
recvx uint // 接收索引
|
|
|
|
|
|
recvq waitq // 等待接收的 goroutine 队列
|
|
|
|
|
|
sendq waitq // 等待发送的 goroutine 队列
|
|
|
|
|
|
lock mutex // 保护所有字段
|
2026-06-07 11:08:10 +08:00
|
|
|
|
}
|
2026-06-07 12:14:39 +08:00
|
|
|
|
```
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
> [!note] 📝 核心考点
|
|
|
|
|
|
> 三个关键点:**环形缓冲区**(高效利用内存)、**两个等待队列**(sendq/recvq,双向链表)、**互斥锁**(保证并发安全)。
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
> [!info] 🔗 延伸阅读
|
|
|
|
|
|
> - [[hzh/GolangStar/Go语言原理/channel原理]] — send/recv 全流程图解
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
---
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
### Q3-Q4:Channel 的发送和读取过程? 🟡中等
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
## 参考答案
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
#### 发送数据流程(按优先级)
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
```mermaid
|
|
|
|
|
|
graph TD
|
|
|
|
|
|
A[发送数据] --> B{recvq 有等待者?}
|
|
|
|
|
|
B -->|是| C[直接传给等待者, 唤醒 receiver]
|
|
|
|
|
|
B -->|否| D{缓冲区有空位?}
|
|
|
|
|
|
D -->|是| E[写入 buf[sendx], 更新索引]
|
|
|
|
|
|
D -->|否| F[创建 sudog 加入 sendq, gopark 阻塞]
|
|
|
|
|
|
C --> G[继续执行]
|
|
|
|
|
|
E --> G
|
|
|
|
|
|
F --> H[被唤醒后继续]
|
|
|
|
|
|
```
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
1. **最优路径**:recvq 有等待者 → 直接拷贝数据给 receiver,跳过缓冲区
|
|
|
|
|
|
2. **正常路径**:缓冲区未满 → 写入 buf[sendx]
|
|
|
|
|
|
3. **阻塞路径**:缓冲区已满 → 加入 sendq,gopark 阻塞等待
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
#### 读取数据流程(按优先级)
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
1. **最优路径**:sendq 有等待者 → 从发送者直接接收数据
|
|
|
|
|
|
2. **正常路径**:缓冲区有数据 → 从 buf[recvx] 取出
|
|
|
|
|
|
3. **阻塞路径**:缓冲区为空 → 加入 recvq,gopark 阻塞
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
> [!warning] ⚠️ 高频陷阱
|
|
|
|
|
|
> 向已关闭的 Channel 发送数据会 panic;从已关闭的 Channel 读取会返回零值和 false。
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
---
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
### Q5:从已关闭的 Channel 还能读出数据吗? 🟢简单
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
## 参考答案
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
**能!** 只要缓冲区还有数据,就能继续读到有效值。只有当 ok == false 时,读出的数据才是无效的。
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
```go
|
|
|
|
|
|
ch := make(chan int, 5)
|
|
|
|
|
|
ch <- 18
|
|
|
|
|
|
close(ch)
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
x, ok := <-ch // x=18, ok=true (还能读到)
|
|
|
|
|
|
x, ok = <-ch // x=0, ok=false (通道空了)
|
|
|
|
|
|
```
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
> [!tip] 💡 面试技巧
|
|
|
|
|
|
> "range ch" 会自动在 ok==false 时退出循环,这是遍历 Channel 的标准写法。
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
---
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
### Q6:Channel 什么情况下引起内存泄漏? 🟡中等
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
## 参考答案
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
最常见的泄漏场景:**goroutine 永久阻塞**。
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
| 泄漏原因 | 示例 |
|
|
|
|
|
|
|---------|------|
|
|
|
|
|
|
| 发送者退出,接收者永远等待 | producer 结束但没关 channel |
|
|
|
|
|
|
| select 无 default 分支 | 所有 case 都无法执行,goroutine 永远阻塞 |
|
|
|
|
|
|
| 未关闭的 channel | consumer 持有引用无法 GC |
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
> [!warning] ⚠️ 高频陷阱
|
|
|
|
|
|
> goroutine 泄漏会导致它所引用的所有变量都无法被 GC 回收,间接造成内存泄漏。
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
> [!tip] 💡 面试技巧
|
|
|
|
|
|
> 回答时给出解决方案:"使用 context.WithTimeout 设置超时"、"确保生产者结束后关闭 channel"、"select 加 default 分支避免死锁"。
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
---
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
### Q7-Q8:关闭 Channel 的异常场景 🟢简单
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
## 参考答案
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
以下操作都会导致 panic:
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
| 操作 | Panic 信息 |
|
|
|
|
|
|
|------|-----------|
|
|
|
|
|
|
| 重复关闭 | `panic: close of closed channel` |
|
|
|
|
|
|
| 关闭 nil Channel | `panic: close of nil channel` |
|
|
|
|
|
|
| 关闭只收 Channel | 编译错误 |
|
|
|
|
|
|
| 往已关闭 Channel 写数据 | `panic: send on closed channel` |
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
---
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
### Q9-Q11:select 的执行机制 🟡中等
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
> [!question] ❓ 思考一下
|
|
|
|
|
|
> 如果多个 case 同时满足条件,select 选哪一个?为什么这样设计?
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
## 参考答案
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
**随机选择。** 如果多个 case 同时就绪,Go 会随机选择一个执行。
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
**设计目的**:避免饥饿问题——防止某个 channel 总是被忽略。
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
select {
|
|
|
|
|
|
case data := <-ch1:
|
2026-06-07 12:14:39 +08:00
|
|
|
|
// 处理 ch1
|
2026-06-07 11:08:10 +08:00
|
|
|
|
case ch2 <- value:
|
2026-06-07 12:14:39 +08:00
|
|
|
|
// 发送数据
|
2026-06-07 11:08:10 +08:00
|
|
|
|
case <-timeout:
|
|
|
|
|
|
// 超时处理
|
|
|
|
|
|
default:
|
2026-06-07 12:14:39 +08:00
|
|
|
|
// 所有 channel 都不可用时执行
|
2026-06-07 11:08:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
#### select 实现原理
|
|
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
|
graph LR
|
|
|
|
|
|
A[编译阶段] --> B[生成 scase 结构体数组]
|
|
|
|
|
|
B --> C[运行时 selectgo]
|
|
|
|
|
|
C --> D[随机排序 case]
|
|
|
|
|
|
D --> E{第一轮扫描}
|
|
|
|
|
|
E -->|找到就绪| F[立即执行]
|
|
|
|
|
|
E -->|全阻塞| G[第二轮: 加入等待队列]
|
|
|
|
|
|
G --> H[gopark 睡眠]
|
|
|
|
|
|
H --> I[channel 就绪后唤醒]
|
|
|
|
|
|
I --> F
|
2026-06-07 11:08:10 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
1. **随机排序**:避免饥饿
|
|
|
|
|
|
2. **两轮扫描**:第一轮直接检查可读写性,找不到则全部加入等待队列后睡眠
|
|
|
|
|
|
3. **scase 结构**:每个 case 封装为 scase,包含 channel 指针、操作类型等信息
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
> [!note] 📝 核心考点
|
|
|
|
|
|
> select 的核心设计:**case 随机化 + 双重循环检测**。面试官可能会追问"为什么不按顺序一个个判断",答案是"为了避免某些 case 长期得不到执行(饥饿)"。
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
## 关联笔记
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
- [[hzh/GolangStar/Go语言进阶/Channel]]
|
|
|
|
|
|
- [[hzh/GolangStar/Go语言进阶/Select]]
|
|
|
|
|
|
- [[hzh/GolangStar/Go语言原理/channel原理]]
|
|
|
|
|
|
- [[hzh/GolangStar/Go面试题库/代码面试题]]
|