Go 八股题库

这份题库重点不是让你背底层源码,而是让你在面试里能把 Go 讲成:

我知道它的核心原理,也知道它为什么适合我项目里的实时链路、高并发服务和消费者。


一、Go 面试里你的最佳定位

你最稳的说法是:

我很多复杂业务经验是在 PHP 上沉淀的,但 Go 在我项目里不是点缀,而是主要承接实时链路、高并发处理、消费者和长生命周期服务。

这句话很重要,因为它诚实,也不会把你讲弱。


二、高频题

1. goroutine 和线程的区别

标准回答

goroutine 是 Go 运行时调度的轻量级执行单元,不等同于操作系统线程。线程是 OS 级别资源,切换和内存开销更大;goroutine 初始栈更小,由 Go runtime 调度到线程上执行,因此在高并发场景下成本更低。

项目里怎么讲

像消费者、批量同步、行为回传这类场景,用 goroutine 更适合做大量并发任务处理,但前提还是要做并发控制,不能无脑起。


2. Go 的 GMP 模型是什么

标准回答

  • G:goroutine
  • M:机器线程
  • P:处理器,上下文调度资源

Go runtime 通过 GMP 模型把大量 goroutine 调度到少量线程上执行,从而兼顾并发能力和资源成本。

面试更稳的说法

我不需要把 runtime 源码背下来,但要理解它为什么能支撑大量并发 goroutine。

如果要讲得更完整,可以这样拆

1. G / M / P 各自是什么

  • G 是 goroutine,也就是用户态任务
  • M 是真正执行代码的 OS 线程
  • P 不是 CPU 核,而是 runtime 的调度上下文和本地运行队列

真正执行时,不是 G 直接跑,而是:

M 必须先拿到 P,才能执行某个 G

2. 为什么需要 P

如果只有 goroutine 和线程,没有 P,那所有 goroutine 都更容易堆到全局队列上,调度竞争会更重,也不容易控制并行度。

P 主要解决 3 件事:

  • 控制并行执行的工作位数量
  • 维护本地运行队列,减少全局锁竞争
  • 把调度能力和线程本身解耦

3. goroutine 是怎么被调度的

一个 goroutine 创建后,通常会先进入某个 P 的本地队列,再由绑定了这个 PM 去执行。

调度时大致会这样:

  • 优先从当前 P 的本地队列取 G
  • 本地没有,再看全局队列
  • 还没有,就可能去别的 P 偷任务,也就是 work stealing

4. 为什么这套模型适合高并发

因为 Go 不是“一个 goroutine 对应一个线程”,而是 runtime 用少量线程去复用大量 goroutine。

这意味着:

  • goroutine 创建更便宜
  • 线程数量更可控
  • 大量并发任务的调度成本更低

5. goroutine 遇到阻塞会怎么样

这里是高频追问。

  • 如果 goroutine 因 channel 阻塞,会被 runtime 挂起,当前 M 可以继续跑别的 G
  • 如果遇到网络 IO,Go 会配合 netpoll,把 goroutine 挂起,等 fd 就绪后再唤醒
  • 如果遇到真正阻塞线程的系统调用,runtime 会尽量把 P 从这个 M 上摘出来,让别的 M 继续跑

一句话总结

GMP 的核心不是记住 3 个字母,而是理解 Go 为什么能用少量线程高效调度大量 goroutine。


3. channel 是什么,有缓冲和无缓冲有什么区别

标准回答

channel 是 goroutine 之间通信和同步的一种机制。

  • 无缓冲 channel:发送和接收必须同步配对
  • 有缓冲 channel:在缓冲区未满前发送不会阻塞,在缓冲区未空前接收不会阻塞

项目里怎么讲

我更关注 channel 的同步语义和限流作用,不会为了用 channel 而用。像 worker pool、任务分发、结果汇总这类场景比较适合。

更完整一点怎么讲

1. channel 不只是“管道”

它更像一个:

  • 数据传递工具
  • goroutine 同步点
  • 背压和限流工具

2. 无缓冲 channel 的本质

无缓冲 channel 更强调“同步交接”。

发送方和接收方必须配对成功,数据才能完成传递,所以它天然带同步语义。

3. 有缓冲 channel 的本质

有缓冲 channel 更像一个有限队列。

  • 缓冲没满时,发送可以先成功
  • 缓冲没空时,接收可以先成功
  • 满了继续发会阻塞
  • 空了继续收会阻塞

4. goroutine 阻塞在 channel 上时发生了什么

如果发送或接收条件不满足,当前 goroutine 不会忙等,而是会被 runtime 挂起,挂到 channel 的等待队列里。

等条件满足后,再被唤醒为 runnable 状态,重新进入调度。

这也是为什么:

  • channel 阻塞的是 goroutine
  • 不一定阻塞整个线程

5. 工程上更适合什么场景

  • worker pool
  • fan-out / fan-in
  • 结果汇总
  • 并发任务限速

不太适合:

  • 特别复杂的共享状态管理
  • 跨很多层乱传,导致链路难懂

4. select 是什么

标准回答

select 用于同时监听多个 channel 的收发操作,哪个 case 先就绪就执行哪个。如果多个同时就绪,会随机选择一个;如果都没就绪,可以配合 default

项目里怎么讲

在处理多个异步结果、超时控制和取消机制时,select 很常用。

这题更完整的说法

1. select 在解决什么问题

goroutine 在工程里经常不是只等一个结果,而是同时等:

  • 正常返回
  • 错误返回
  • 超时信号
  • 取消信号
  • 退出信号

这时候 select 就是最自然的多路等待机制。

2. 为什么多个异步结果很适合 select

比如你同时请求多个下游,谁先回来先处理,或者先收集一部分结果再汇总,这种“谁先 ready 就处理谁”的模型特别适合 select

3. 超时控制为什么常和 select 一起用

常见写法是:

  • case res := <-resultCh
  • case <-time.After(...)
  • case <-ctx.Done()

这背后的含义是:

  • 正常结果到了就走成功分支
  • 到了超时时间就主动放弃
  • 上游取消了就尽快退出

4. default 的作用是什么

defaultselect 变成非阻塞检查。

但也要小心,如果写不好,很容易变成空转循环,把 CPU 打高。

一句更像做过项目的话

select 的价值不只是语法糖,它让 Go 能很自然地表达多路异步等待、超时控制和取消传播。


5. channel close 后会怎样

标准回答

  • 关闭后的 channel 不能再发送数据,发送会 panic
  • 关闭后的 channel 仍然可以接收,直到缓冲区读完
  • 接收完后继续读会得到零值和 ok=false

6. context 是什么,为什么重要

标准回答

context 用于在请求链路中传递超时、取消信号和少量上下文数据。尤其在微服务、HTTP 调用、数据库调用、Go 并发任务里,context 是控制请求生命周期的重要工具。

项目里怎么讲

Go 在实时链路和消费者场景里,如果没有 context 做超时和取消,长链路任务很容易失控。

进一步怎么解释

context 在 Go 里之所以重要,不只是因为它能“传参数”,而是因为它把:

  • 超时
  • 主动取消
  • 请求范围内的链路控制

统一成了一套约定。

面试里很常见的配套说法是:

context 的 Done 本质上就是一个只读 channel,所以它天然适合和 select 一起使用。

这就是为什么 Go 里常见写法总是:

  • 等正常结果
  • 同时等 ctx.Done()
  • 必要时再等超时信号

7. mutex、rwmutex、atomic 分别适合什么场景

标准回答

  • mutex:简单互斥保护
  • rwmutex:读多写少场景下允许并发读
  • atomic:适合简单数值或状态位的原子操作

面试更稳的说法

不是 atomic 越底层越高级,而是要看场景。复杂共享结构还是锁更清晰。


8. 为什么 map 并发不安全

标准回答

Go 原生 map 在并发读写时没有内部同步保护,并发写或者并发读写可能导致 panic 或数据不一致,因此需要自己加锁或使用 sync.Map

追问:sync.Map 适合什么场景

适合读多写少、key 相对稳定的场景,不适合所有业务 map 一律替换。

如果要讲得更详细,可以这样拆

1. 先讲清楚“到底什么不安全”

不是所有并发访问都不行。

更准确的说法是:

  • 多个 goroutine 只读同一个 map,一般是安全的
  • 只要有 goroutine 在写,同时还有别的 goroutine 读或写,就不安全

也就是:

map 真正危险的是并发写,以及读写并发。

2. 为什么不安全

因为 Go 原生 map 本质上是一个运行时维护的哈希表结构,它在写入时不只是“改一个值”,还可能伴随很多内部操作,比如:

  • bucket 内元素插入
  • 溢出 bucket 分配
  • 扩容
  • 数据迁移,也就是 evacuation
  • 哈希状态和元数据更新

这些动作如果和另一个 goroutine 的读或写交叉在一起,就可能出现:

  • 读到一半迁移中的数据
  • bucket 状态不一致
  • 结构被改到一半
  • 最后触发 runtime 检测错误,直接 panic

3. 为什么 Go 不给 map 自动加锁

因为 map 是非常基础、非常高频的数据结构。

如果给所有 map 内置锁,会带来:

  • 额外内存开销
  • 每次访问都要付同步成本
  • 简单单线程场景也被拖慢

Go 的设计选择是:

把同步控制交给使用者,而不是把所有 map 都做成重型并发容器。

4. 面试里可以怎么更稳地说

Go 原生 map 不是并发容器,它只是普通哈希表。写 map 时可能触发 bucket 插入、扩容和迁移,这些过程没有内建同步保护,所以并发写或读写并发会破坏内部状态,严重时 runtime 会直接报 concurrent map read and map write。

5. 遇到这个问题一般怎么解决

常见方案有 3 类:

方案 1:map + sync.RWMutex

最常见,也最容易理解。

适合:

  • 业务结构清晰
  • 需要强类型
  • 读写比例虽然偏读,但也不是极端读多写少
方案 2:分片 map

把一个大 map 按 key hash 到多个小分片,每个分片一把锁。

适合:

  • 并发比较高
  • 单把大锁竞争明显
方案 3:sync.Map

适合:

  • 读多写少
  • key 集合比较稳定
  • 更像缓存、注册表、懒加载结果表

6. sync.Map 为什么不适合到处替换原生 map

因为它不是“更高级的 map”,而是针对特定并发模式优化过的专用容器。

它的代价包括:

  • API 不如原生 map 直接
  • 失去普通 map 的强类型体验,需要断言
  • 写多、删多、key 变化频繁时不一定更好

追问:sync.Map 到底适合什么场景

更完整的回答可以这样说:

sync.Map 更适合读多写少、key 相对稳定、多个 goroutine 高频读取的场景,比如本地缓存、配置快照、连接注册表、已经初始化好的对象池索引。它不适合业务里所有共享 map 都一把梭替换,因为在写多删多、需要复杂复合操作或很强调类型安全的场景,map 加锁通常更清晰。


9. slice 和 array 的区别

标准回答

  • array 长度固定,是值类型
  • slice 是对底层数组的轻量抽象,包含指针、长度、容量,更灵活

追问:slice 扩容原理

扩容时会分配更大的底层数组并拷贝数据。不同版本 Go 的扩容策略细节不完全一样,但总体是小容量时倍增,大容量时增长比例变缓。


10. defer、panic、recover 分别做什么

标准回答

  • defer:延迟执行,常用于资源释放、解锁、日志
  • panic:程序异常中断
  • recover:在 defer 中捕获 panic,避免程序直接崩溃

面试更稳的说法

业务代码里不应该滥用 panic,把 panic 当普通错误处理;panic 更适合不可恢复的严重异常。


11. Go 的 interface 底层大概怎么理解

标准回答

interface 本质上保存两部分:

  • 类型信息
  • 数据指针

空接口和非空接口的内部结构略有差异,但核心都是通过运行时类型信息决定如何调用具体实现。

常见追问:为什么会有 nil interface 坑

因为“接口本身为 nil”和“接口里装的是一个 typed nil”不是一回事。


12. Go GC 你怎么理解

标准回答

Go GC 是并发标记清扫为主的垃圾回收机制,目标是在尽量控制停顿时间的同时回收不再使用的对象。面试里不用背太深源码,但要知道:

  • 会有 GC 开销
  • 高分配场景要关注对象创建和逃逸
  • 长生命周期服务更要关心内存行为

13. 什么是逃逸分析

标准回答

编译器会分析变量是否可以分配在栈上,如果变量在函数外部还会被引用,就可能逃逸到堆上。堆分配更多,GC 压力也更大。

项目里怎么讲

高并发服务里,我会注意不要频繁创建大对象、避免不必要的堆分配。


14. worker pool 为什么常见

标准回答

worker pool 的核心价值是控制并发度,避免无上限创建 goroutine,把任务放进队列,由固定数量的 worker 消费。

项目里怎么讲

像 Kafka 消费、批量回传、并发请求外部接口这类场景,用 worker pool 比无脑起 goroutine 更稳。


15. Go 里怎么做超时、重试和限流

标准回答

  • 超时:context.WithTimeout
  • 重试:控制次数、间隔、幂等前提
  • 限流:令牌桶、漏桶、并发控制、队列控制

面试更稳的说法

真正重要的不是“有重试”,而是重试是否幂等、是否会放大故障、是否配合超时与熔断。


16. 你项目里 Go 主要用在哪里

这是你很可能会被问到的。

推荐回答

我项目里 Go 主要不是做普通后台 CRUD,而是承接更适合服务化和高并发的部分。比较典型的是推广 ROI 这条线,Go 主要用在点击和行为数据处理、Kafka 消费者、实时回传、批量任务和部分长链路服务;另外像一些高并发、脚本耗时重的能力,也会从 PHP 侧迁到 Go。


三、Go 面试里你最该主动讲的点

1. 为什么这块要用 Go,不继续用 PHP

你可以说:

不是为了换语言,而是因为实时链路、高并发处理、消费者和长生命周期服务更适合 Go。PHP 更适合复杂业务规则和后台系统,两边在系统里是分工关系。

2. 你不是“只会背原理”

你可以说:

我对 Go 的理解更偏工程落地,比如并发怎么控、任务怎么拆、消费者怎么稳、服务怎么治理。


四、Go 继续追问时的答法

追问 1:Go 怎么保证高并发一定更好

可以答:

Go 不是天然就比别的快,关键还是看场景。它在 goroutine、channel、runtime 调度、常驻服务和资源利用率上更适合做消费者、实时链路和高并发任务。

追问 2:Go 服务如果积压了怎么办

可以答:

先看积压发生在哪一层,是消费速度不够、下游慢、重试过多,还是分区不合理。再决定是扩 consumer、优化处理逻辑、增加限流还是拆分链路。

追问 3:你在 Go 项目里怎么做可观测性

可以答:

我比较关注 traceId、关键日志、消费延迟、失败数、重试数和外部依赖超时。高并发系统如果没这些,排查会很痛苦。


五、你复习 Go 最该背的 12 个点

  1. goroutine 和线程区别
  2. GMP 模型
  3. channel
  4. select
  5. close 后行为
  6. context
  7. mutex / rwmutex / atomic
  8. map 并发问题
  9. slice 扩容
  10. defer / panic / recover
  11. interface / nil 坑
  12. worker pool / 超时 / 重试 / 限流