并发编程深入:channel与select用法、sync包(Mutex/RWMutex/Cond)、并发模式(Fan-in/Fan-out)

本章覆盖 Go 并发编程三大高频考点:channel + select 的消息通信机制、sync 包(Mutex/RWMutex/Cond)的同步控制、以及 Fan-in / Fan-out 经典并发模式。面试时不仅要能写出正确代码,还需讲清选型依据、场景边界和性能影响。以下先给出核心回答框架,再深入三组问答卡片与真实项目模板。

Go 并发编程 channel + select sync 包 Fan-in / Fan-out ⚠️ 选型关键:通信 vs. 互斥、同步 vs. 异步、扇出 vs. 扇入

面试问答卡片

面试问题:请用 channel 和 select 实现一个超时控制的任务队列,如何优雅退出?

面试官考察点:

  • 是否理解 select 的随机选择与 default 的含义
  • 能否处理 channel 关闭后的读取行为
  • 优雅退出(Goroutine 泄漏预防)

回答框架:

  1. 定义带缓冲 channel 接收任务,另设一个 time.After channel 做超时。
  2. select 中优先处理任务,超时触发兜底逻辑。
  3. 用 close(ch) 广播退出信号,结合 sync.WaitGroup 等待所有 worker 结束。

简短示例回答:“创建一个任务 channel,worker 用 for-select 循环。select 中 case 取任务执行,case <-time.After(5s) 则记录超时。主程序 close(ch) 并 wg.Wait() 确保 worker 全部退出。注意从关闭 channel 读到零值时要标记终止。”

可能追问:

  • 如何避免 task 积压导致内存爆炸?(背压控制)
  • 多个 worker 如何均匀分配任务?
  • 选 buffer=1 还是 buffer=100?依据。
避坑提醒:select 在 nil channel 上永远阻塞;关闭 channel 后 select 会立即返回零值,需用 ok 判断或额外标记。不要在同一协程中读写未初始化的 channel。

面试问题:sync.Mutex、sync.RWMutex、sync.Cond 各自适用什么场景?请举例说明。

面试官考察点:

  • 能否区分读多写少与写多读少场景
  • 是否理解条件变量的“等待-通知”机制
  • 能否避免死锁和信号丢失

回答框架:

  1. Mutex:互斥锁,适合临界区短、读写比例接近的场景。
  2. RWMutex:读写锁,读操作无竞争时性能优于 Mutex(读多写少)。
  3. Cond:条件变量,配合 Mutex 实现等待资源可用/任务就绪(如批量唤醒)。

简短示例回答:“Mutex 用于保护共享计数器;RWMutex 用于缓存场景,读并发高、写极低;Cond 用于等待队列非空时唤醒消费者,避免轮询浪费CPU。”

可能追问:

  • RWMutex 写锁饥饿问题如何解决?
  • Cond 的 Signal 和 Broadcast 区别?
  • 什么时候用 atomic 代替 Mutex?
避坑提醒:Cond 必须先加锁再 Wait;Signal 可能丢失(如果无等待协程)。RWMutex 写锁等待期间新读锁会被阻塞(Go 1.8+ 公平调度)。避免在持有锁时调用外部可能阻塞的函数。

面试问题:请用 Go 实现一个 Fan-out / Fan-in 模式,并说明如何保证全部结果正确合并。

面试官考察点:

  • 是否理解扇出(分发)和扇入(合并)的同步策略
  • 能否处理 Goroutine 错误传播与取消
  • 对 context 和 errgroup 的了解

回答框架:

  1. Fan-out:启动多个 worker goroutine,从同一个 source channel 读取任务。
  2. Fan-in:创建合并 channel,所有 worker 将结果写入该 channel,用 sync.WaitGroup 等待,最后关闭。
  3. 错误处理:使用 errgroup 或带 error channel 的 select。

简短示例回答:“定义 source chan 分发任务,启动 N 个 worker。每个 worker 处理后将结果发送到 results chan。主协程用 wg.Wait() 等待,然后 close(results)。最后从 results 中 collect 所有数据。配合 errgroup 捕获首个错误并 cancel context。”

可能追问:

  • 其中一个 worker 失败如何让整体终止?
  • 如何避免 results channel 阻塞导致 worker 泄漏?
  • 扇出数量与 CPU 核数的关系?
避坑提醒:Fan-in 时必须在所有生产者结束后才 close 结果 chan,否则 panic。使用 buffered channel 可减少生产/消费速率不匹配时的死锁风险。优先使用 errgroup.WithContext 管理取消。

项目案例:实时日志聚合与分析平台

维度 内容
项目名称 OceanLog – 实时日志聚合与分析平台
业务背景 微服务架构下,数千个服务实例产生海量日志,需要近实时聚合、清洗、存储到 Elasticsearch,并支持告警与离线分析。
规模指标 日均处理日志 10TB,峰值 10 万条/秒,集群 50 个节点,单个节点吞吐 ≥ 2000 msg/s。
技术栈 Go 1.19、gRPC、Kafka(数据源)、Redis(去重/限流)、Elasticsearch(存储)、Prometheus(监控)
核心难点
  • 乱序与重试:日志到达顺序不确定,需用时间戳+ID 去重。
  • 背压控制:ES 写入慢时需反压上游,避免内存 OOM。
  • 资源隔离:不同业务线日志不能互相阻塞。
  • 优雅退出:重启时不能丢失正在处理的日志。
面试高频追问
  • Fan-out 时 worker 数量如何动态调整?
  • 如何保证至少一次投递?
  • select 中混合业务信号与 system signal 处理。
  • Cond 用于 worker 动态扩缩容的实践。
可迁移回答话术 “我们使用 Fan-out 模式将 Kafka 消费到的日志扇出到多个处理 worker;每个 worker 内部用带缓冲的 channel 做队列,select 监听任务和退出信号。Fan-in 通过一个合并 channel 统一写入 ES,利用 sync.WaitGroup 确保所有数据 flush。为防止背压,使用带 timeout 的 context 和动态调整 worker 数(通过 sync.Cond 广播通知 worker 增减)。核心设计原则:缓冲 + 背压 + 优雅关闭。”
面试加分提示:在回答并发模式时主动提及 errgroup、context(含 cancel / timeout)、select 中 nil channel 的用法(动态开关分支),能体现对 Go 并发模型的高级理解。