Skip to content

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:

  • 缓冲未满时发送可直接进入队列。
  • 缓冲非空时接收可直接取值。
  • 容量用于吸收短期流量差,不应被当作无限队列。
go
unbuffered := make(chan int)
buffered := make(chan int, 100)

10.3 向 nil、已关闭 channel 操作会怎样? ​

操作nil channel已关闭 channel
接收永久阻塞先取完缓冲数据且 ok=true,之后立即返回零值与 ok=false
发送永久阻塞panic
closepanic再次 close 会 panic

nil channel 在 select 中对应分支永远不会就绪,可以用于动态启用或禁用 case。

10.4 谁应该关闭 channel? ​

通常由发送方关闭,因为发送方知道是否还会产生数据。原则:

  • 接收方一般不关闭 channel。
  • 多发送方场景不要让任意一个发送者直接关闭共享 channel。
  • 使用协调者、WaitGroup 或单独所有者,在所有发送者退出后关闭。
  • channel 不一定必须关闭;只有接收方需要知道“不再有数据”时才需要。

10.5 range channel 什么时候结束? ​

只有在 channel 被关闭且缓冲区数据全部取完后结束。

go
for v := range ch {
	consume(v)
}

如果 channel 永不关闭,range 可能永久阻塞。

10.6 select 的行为是什么? ​

  • 没有 case 就绪:阻塞。
  • 多个 case 同时就绪:伪随机选择一个,不提供严格公平、优先级或有限时间内必被选择的保证。
  • 有 default 且无其他 case 就绪:执行 default,不阻塞。
  • 对 nil channel 的 case 永远不就绪。
go
select {
case v := <-ch:
	_ = v
case <-ctx.Done():
	return ctx.Err()
}

10.7 使用 default 有什么坑? ​

循环中使用 select { default: } 可能形成忙循环,占满 CPU:

go
for {
	select {
	case v := <-ch:
		_ = v
	default:
		// 持续空转
	}
}

如果只是等待事件,不要加 default;如果确实要轮询,应加入 ticker、退避或其他阻塞点。

10.8 如何实现超时? ​

一次性超时:

go
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 在请求链路中传递:

  • 取消信号。
  • 截止时间和超时。
  • 请求级元数据。

典型签名:

go
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 和父子关联资源:

go
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
defer cancel()

11.4 context 取消会强行杀死 goroutine 吗? ​

不会。取消只关闭 ctx.Done() 并设置 ctx.Err()。业务代码必须主动监听并退出:

go
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 应使用包内自定义不可导出类型,避免冲突:

go
type contextKey struct{}
ctx = context.WithValue(ctx, contextKey{}, value)

11.6 为什么不能随意使用 context.Background()? ​

在已有请求链路中换成 Background 会切断上游取消、超时和追踪信息,造成请求已结束但下游仍继续执行。

只有明确要脱离请求生命周期的任务才考虑构造独立 context,同时要为它建立自己的超时、所有者和关闭流程。


继续阅读