Go 后端面试复盘:语言、并发与服务实践 · 10. Channel 与 select至11. Context
前置:Go 系统学习,并完成一个HTTP 工程练习。本页用于面试前查漏补缺,不按章节替代语言课程。
10. Channel 与 select
10.1 channel 的作用是什么?
channel 用于 goroutine 之间传递值和建立同步关系。Go 的经典理念是“通过通信共享内存”,但实际工程中 channel 和 mutex 各有适用场景。
- channel:所有权转移、任务流水线、事件通知。
- mutex:保护共享状态、短临界区、随机访问数据。
10.2 无缓冲和有缓冲 channel 有什么区别?
无缓冲 channel:
- 发送和接收必须同时准备好,完成一次同步交接。
- 发送操作先行于对应接收的完成;无缓冲通道还保证接收先行于对应发送的完成。这里只指通信边界,不保证双方后续业务代码的先后。依据:Go 内存模型。
有缓冲 channel:
- 缓冲未满时发送可直接进入队列。
- 缓冲非空时接收可直接取值。
- 容量用于吸收短期流量差,不应被当作无限队列。
unbuffered := make(chan int)
buffered := make(chan int, 100)10.3 向 nil、已关闭 channel 操作会怎样?
| 操作 | nil channel | 已关闭 channel |
|---|---|---|
| 接收 | 永久阻塞 | 先取完缓冲数据且 ok=true,之后立即返回零值与 ok=false |
| 发送 | 永久阻塞 | panic |
| close | panic | 再次 close 会 panic |
nil channel 在 select 中对应分支永远不会就绪,可以用于动态启用或禁用 case。
10.4 谁应该关闭 channel?
通常由发送方关闭,因为发送方知道是否还会产生数据。原则:
- 接收方一般不关闭 channel。
- 多发送方场景不要让任意一个发送者直接关闭共享 channel。
- 使用协调者、
WaitGroup或单独所有者,在所有发送者退出后关闭。 - channel 不一定必须关闭;只有接收方需要知道“不再有数据”时才需要。
10.5 range channel 什么时候结束?
只有在 channel 被关闭且缓冲区数据全部取完后结束。
for v := range ch {
consume(v)
}如果 channel 永不关闭,range 可能永久阻塞。
10.6 select 的行为是什么?
- 没有 case 就绪:阻塞。
- 多个 case 同时就绪:伪随机选择一个,不提供严格公平、优先级或有限时间内必被选择的保证。
- 有 default 且无其他 case 就绪:执行 default,不阻塞。
- 对 nil channel 的 case 永远不就绪。
select {
case v := <-ch:
_ = v
case <-ctx.Done():
return ctx.Err()
}10.7 使用 default 有什么坑?
循环中使用 select { default: } 可能形成忙循环,占满 CPU:
for {
select {
case v := <-ch:
_ = v
default:
// 持续空转
}
}如果只是等待事件,不要加 default;如果确实要轮询,应加入 ticker、退避或其他阻塞点。
10.8 如何实现超时?
一次性超时:
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case v := <-ch:
_ = v
case <-timer.C:
return ErrTimeout
}在高频循环中反复调用 time.After 会反复创建 timer,最好复用 time.Timer。重置 timer 时要理解 Stop、通道排空和 Reset 的版本语义。
10.9 如何实现 fan-out / fan-in?
- fan-out:多个 worker 从同一输入 channel 取任务,提升并行度。
- fan-in:多个结果 channel 汇总到一个输出 channel。
必须同时处理:
- context 取消。
- 发送端阻塞。
- worker 错误。
- 输出 channel 关闭时机。
- 最大并发和背压。
查看图表代码
10.10 channel 缓冲应该设置多大?
没有通用答案。缓冲区表达允许生产者领先消费者多少:
- 太小:频繁阻塞、吞吐可能下降。
- 太大:隐藏下游过载、增加延迟和内存,故障时积压更多。
应根据到达速率、处理速率、允许延迟、单项大小和突发量估算,再压测验证。长期平均生产速率超过消费速率时,任何有限缓冲最终都会满。
11. Context
11.1 context 解决什么问题?
context 在请求链路中传递:
- 取消信号。
- 截止时间和超时。
- 请求级元数据。
典型签名:
func Do(ctx context.Context, req Request) (Response, error)context 通常作为第一个参数,不存进结构体,不传 nil。
11.2 Background 和 TODO 有什么区别?
context.Background():明确的根 context,常用于 main、初始化和顶层任务。context.TODO():当前不知道应该传哪个 context,作为临时占位,提醒后续完善。
二者行为接近,但表达的设计意图不同。
11.3 WithCancel、WithTimeout、WithDeadline 的区别?
WithCancel:由调用方主动取消。WithTimeout:从现在开始一段时间后取消。WithDeadline:指定绝对截止时间。
创建后应调用返回的 cancel,及时释放 timer 和父子关联资源:
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
defer cancel()11.4 context 取消会强行杀死 goroutine 吗?
不会。取消只关闭 ctx.Done() 并设置 ctx.Err()。业务代码必须主动监听并退出:
select {
case <-ctx.Done():
return ctx.Err()
case result := <-resultCh:
return use(result)
}如果底层调用不支持 context 或代码从不检查 Done,取消无法使其停止。取消本身也不等待任务退出,仍需 WaitGroup 或任务组完成等待。
11.5 context value 应该放什么?
适合:
- trace ID。
- request ID。
- 认证主体。
- 请求范围元数据。
不适合:
- 可选函数参数。
- 全局配置。
- 数据库连接池。
- 大对象。
- 业务必填参数。
key 应使用包内自定义不可导出类型,避免冲突:
type contextKey struct{}
ctx = context.WithValue(ctx, contextKey{}, value)11.6 为什么不能随意使用 context.Background()?
在已有请求链路中换成 Background 会切断上游取消、超时和追踪信息,造成请求已结束但下游仍继续执行。
只有明确要脱离请求生命周期的任务才考虑构造独立 context,同时要为它建立自己的超时、所有者和关闭流程。
继续阅读
- 排错
- 排错
- 排错