Go 八股题库
这份题库重点不是让你背底层源码,而是让你在面试里能把 Go 讲成:
我知道它的核心原理,也知道它为什么适合我项目里的实时链路、高并发服务和消费者。
一、Go 面试里你的最佳定位
你最稳的说法是:
我很多复杂业务经验是在 PHP 上沉淀的,但 Go 在我项目里不是点缀,而是主要承接实时链路、高并发处理、消费者和长生命周期服务。
这句话很重要,因为它诚实,也不会把你讲弱。
二、高频题
1. goroutine 和线程的区别
标准回答
goroutine 是 Go 运行时调度的轻量级执行单元,不等同于操作系统线程。线程是 OS 级别资源,切换和内存开销更大;goroutine 初始栈更小,由 Go runtime 调度到线程上执行,因此在高并发场景下成本更低。
项目里怎么讲
像消费者、批量同步、行为回传这类场景,用 goroutine 更适合做大量并发任务处理,但前提还是要做并发控制,不能无脑起。
2. Go 的 GMP 模型是什么
标准回答
G:goroutineM:机器线程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 的本地队列,再由绑定了这个 P 的 M 去执行。
调度时大致会这样:
- 优先从当前
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 := <-resultChcase <-time.After(...)case <-ctx.Done()
这背后的含义是:
- 正常结果到了就走成功分支
- 到了超时时间就主动放弃
- 上游取消了就尽快退出
4. default 的作用是什么
default 让 select 变成非阻塞检查。
但也要小心,如果写不好,很容易变成空转循环,把 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 个点
- goroutine 和线程区别
- GMP 模型
- channel
- select
- close 后行为
- context
- mutex / rwmutex / atomic
- map 并发问题
- slice 扩容
- defer / panic / recover
- interface / nil 坑
- worker pool / 超时 / 重试 / 限流