适用读者:Java 背景转 Go、或要答"Go 和 Java 有什么不同"的人。
前置:S09(Go 系列)、S25-J07(并发题库)。
覆盖旧文:3506(Go 语言面试题,约 15 个专题、200+ 问,60,244 字符)→ 1 篇/整篇题集。
扩展点:E-S25-044(对照式答法:把 Go 的每题挂到熟悉的 Java 概念上)、E-S25-045(slice/map/channel 三个"必被追问"的底层)、E-S25-046(GMP 与 Java 线程模型的差别)。
上一篇:S25-J13 · 高并发系统设计题:估算 → 瓶颈 → 取舍
下一篇:题库层最后一批 · LLM/Agent 题库(待写)。
1. 一句话结论
答 Go 题的最优姿势是"对照 Java":"Go 的 slice 类似 Java 的 ArrayList 但共享底层数组"——一句话就让面试官知道你两边都懂。只背 Go 语法的人,答不出"为什么这样设计"。
出处:
A-3506(Go 语言面试题,15 专题)。
2. 对照表:Go ↔ Java(E-S25-044)
| 概念 | Go | Java |
|---|---|---|
| 并发单元 | goroutine(用户态,KB 级栈) | 平台线程(MB 级栈)/ 虚拟线程(JDK 21) |
| 调度 | GMP(用户态调度器) | OS 调度(虚拟线程有 ForkJoin 载体) |
| 通信 | channel(CSP) | BlockingQueue / 共享内存 + 锁 |
| 泛型 | 1.18 引入(类型参数 + 约束) | 泛型(类型擦除) |
| 错误处理 | error 返回值 + panic/recover |
异常(checked/unchecked) |
| 内存回收 | 并发三色标记 + 混合写屏障 | 分代 GC(G1/ZGC) |
| 继承 | 组合 + 接口(隐式实现) | 类继承 + 接口 |
判据:"Go 没有继承、没有异常、没有分代 GC"——每条都要能说出"那它用什么替代、代价是什么"。
3. 三个必被追问的底层(E-S25-045)
3.1 slice:共享底层数组是双刃剑
s := make([]int, 3, 5) // len=3 cap=5
s2 := s[1:3] // 共享底层数组,改 s2[0] 会改 s[1]
s3 := append(s, 9) // cap 够 → 原地写;不够 → 新数组(此后不共享)
| 问 | 答 |
|---|---|
| 底层结构 | {ptr, len, cap} 三个字 |
| 扩容规则 | 需容量 < 256 翻倍;≥256 按 ~1.25 倍增长(版本相关,1.18 后调整过) |
| 传参会不会改原 slice | 修改元素会(共享数组);append 扩容后不会 |
copy vs append |
copy 不扩容、目标长度由 len 决定 |
实测口径:扩容倍率随 Go 版本调整,答题时说"约 1.25 倍 + 版本相关"比给精确值安全。
3.2 map:无序是设计选择
| 问 | 答 |
|---|---|
| 底层 | hmap + bmap 桶(8 个 kv/桶)+ 溢出桶 |
| 为什么无序 | 故意随机化起始桶,防止业务依赖遍历顺序(Go 1.0 起) |
| 并发安全 | 否。并发读写会 fatal error: concurrent map writes(不是 panic 可 recover) |
| 安全替代 | sync.Map(读多写少)或 RWMutex + map |
| 扩容 | 装载因子 > 6.5 触发翻倍;过多溢出桶触发等量扩容(整理) |
| 渐进式迁移 | hmap.oldbuckets,每次操作搬迁 1~2 个桶 |
加分点:sync.Map 用 read(原子读)+ dirty 双层,适合"读多写少、key 稳定";写多场景性能不如加锁 map。
3.3 channel:关闭与泄漏
| 问 | 答 |
|---|---|
| 底层 | hchan:环形缓冲区 + sendq/recvq 等待队列 + 锁 |
| 从已关闭 channel 读 | 返回零值 + ok=false(不阻塞) |
| 往已关闭 channel 写 | panic |
| 重复关闭 | panic |
| 常见泄漏 | 无缓冲 channel 无人接收 → goroutine 永久阻塞;select 缺 default/超时 |
判据:"channel 不会自己关,谁写谁关,且只能关一次"——三句话答完 90% 的追问。
4. GMP 与 Java 线程模型的差别(E-S25-046)
G (goroutine) → 待执行的任务
M (machine) → OS 线程
P (processor) → 逻辑处理器,持有本地运行队列(默认 = GOMAXPROCS = CPU 核数)
| 维度 | Go GMP | Java 平台线程 |
|---|---|---|
| 调度层 | 用户态(M:N) | 内核态(1:1) |
| 栈 | 初始 2 KB,可增长 | 固定 1 MB(默认) |
| 切换成本 | 低(无需陷入内核) | 高(内核态切换) |
| 阻塞处理 | netpoller 把阻塞 G 剥离,M 继续跑别的 G |
线程阻塞即占一个内核线程 |
| 抢占 | 1.14+ 基于信号的异步抢占 | OS 时间片 |
Java 21 虚拟线程是同类思路(M:N 用户态调度),但 Go 的 channel/select 是语言级原语,Java 靠 BlockingQueue。
5. 高频"陷阱题"速查
| 题 | 陷阱 |
|---|---|
defer 执行顺序 |
LIFO;参数在 defer 语句处求值,不是函数返回时 |
| 两个 nil 接口相等吗 | 不一定:iface 的 (type,value) 都 nil 才相等;var e error = (*MyErr)(nil) 不等于 nil |
%v/%+v/%#v |
值 / 带字段名 / 带类型(Go 语法表示) |
struct{} 占空间吗 |
占 0 字节;用于 map[string]struct{} 当 set |
| 传参值还是引用 | 都是值传递;slice/map/channel 传的是"描述符的副本" |
| 变量在栈还是堆 | 逃逸分析决定;go build -gcflags='-m' 可看 |
init() 时机 |
包级变量初始化后、main 前;同包多文件按文件名顺序 |
| 字符串拼接 | +(少量)/ strings.Builder(循环)/ bytes.Buffer;Builder 避免多次分配 |
6. 反例:Java 思维答题的三个坑
| 说法 | 问题 |
|---|---|
| "Go 的锁和 Java 一样是重入锁" | sync.Mutex 不可重入,重入即死锁 |
| "Go 有 try-catch" | 只有 panic/recover,且不能跨 goroutine |
| "GC 也是分代的" | Go 是并发三色标记 + 混合写屏障,不分代(1.19 后有 GC 优化但无分代收集) |
7. 自测
- slice 扩容后为什么不再共享底层数组?
- Go 的 map 为什么故意设计成无序?
- 往已关闭 channel 写、重复关闭分别会怎样?
- GMP 的 M:N 与 Java 平台线程 1:1 的差别体现在哪?
sync.Mutex可重入吗?
答案
- 容量不足时
append分配新数组并复制,指针改变。 - 防止业务依赖遍历顺序(随机起始桶)。
- 写 → panic;重复关闭 → panic。
- 用户态调度、栈 KB 级、切换不进内核;阻塞由 netpoller 剥离。
- 不可重入,重入会死锁。
8. 下一篇
题库层最后一批 · LLM 与 Agent 题库(约 150 篇编号题,35xx 系列)—— RAG、Agent 记忆、工具调用、评估与部署的答题骨架(待写)。
本篇属于系列《S25 · 求职与面试》。旧文来源:A-3506、A-3484;扩展点 E-S25-044/045/046。容量数字为按公式估算(非线上实测)。