Mark

完整面试流程与通用追问

本章给出一场可执行的模拟面试,以及根据回答改变方向的题目树。技术题提供参考答案;个人贡献、生产规模和求职意愿没有统一正确答案,相关位置必须换成真实情况。

三个项目的 go.mod 分别声明 Go 1.26.6、1.26.3、1.26.5。本章讨论稳定语义,涉及循环变量或容器默认值时会注明版本背景,不用过时八股替代项目事实。

1 一场 90 分钟技术面的安排

时间 面试官的动作 候选人的目标 进入哪个分支
0–5 分钟 自我介绍,确认毕业与到岗时间 用 60–90 秒建立方向,留下一个可深挖的问题 M1
5–10 分钟 核验项目起点、个人贡献与 AI 使用 说清本人做了什么、哪些证据可核验 M2
10–35 分钟 深挖一个主项目 从完整链路进入两条故障时间线和一个权衡 项目章节任选一章
35–47 分钟 交叉问第二项目或 Agent 实习 验证理解能否迁移,避免只背一个故事 另一项目或实习章节
47–65 分钟 Go、数据库、网络与排障 解释语义、写出关键代码或 SQL,识别失败边界 G、D、R、H、O 中选 4–6 题
65–80 分钟 手写代码并测试 先定契约,再实现,再检查边界 手写题章节 K1 或 K2
80–87 分钟 协作、困难、反思、岗位动机 真实经历与具体行动,避免空泛价值观 行为面章节 B、C
87–90 分钟 候选人反问 判断工作内容、培养与工程方式是否合适 行为面章节 Q

这是一场模拟安排,不是所有公司的固定流程。实际校招可能把它拆成笔试、一面、二面和 HR 面。题库用于按分支练习,不要求在一场面试里问完。

多轮练习建议:一面用“一个项目 + Go/SQL + 编码”;二面用“故障分析 + 系统设计 + 另一个项目/实习”;HR 面核实时间、协作和动机,不把技术答案重新背一遍。

2 总体追问树

面试开始
├─ M1 请介绍自己
│  ├─ M1.1 为什么选这个主项目
│  └─ M1.2 为什么选择 Go 后端
├─ M2 哪些工作由你完成
│  ├─ M2.1 如果大量使用 AI 你如何证明理解
│  │  └─ M2.1.1 现在现场改一个失败条件
│  └─ M2.2 你说验证过 请给具体证据
├─ 主项目深挖
│  ├─ 讲 Flostra → 04 的 F 系列
│  ├─ 讲 Nyauth → 03 的 Q 系列
│  ├─ 讲 Gline → 05 的 G 系列
│  └─ 讲 Agent 实习 → 06 的 A 系列
├─ 通用基础
│  ├─ G1 goroutine 生命周期
│  │  ├─ G1.1 context 能强制停止吗
│  │  │  └─ G1.1.1 无法取消的调用怎么处理
│  │  └─ G1.2 谁关闭 channel
│  ├─ G2 共享状态与数据竞争
│  │  ├─ G2.1 channel 还是 Mutex
│  │  └─ G2.2 race 通过等于没有并发问题吗
│  │     └─ G2.2.1 数据竞争与业务竞态有什么不同
│  ├─ G3 调度与内存
│  │  ├─ G3.1 GOMAXPROCS 控制什么
│  │  └─ G3.2 内存上涨怎么定位
│  │     └─ G3.2.1 何时调整 GC 或使用 Pool
│  └─ G4 类型与错误
│     ├─ G4.1 typed nil 为什么不等于 nil
│     ├─ G4.2 slice 复制是否隔离
│     └─ G4.3 错误包装与 panic 如何取舍
├─ 数据与网络
│  ├─ D1 事务为什么仍会出并发错误
│  │  ├─ D1.1 条件更新还是行锁
│  │  └─ D1.2 隔离级别如何选
│  │     └─ D1.2.1 事务失败后如何重试
│  ├─ D2 查询慢怎么优化
│  │  ├─ D2.1 联合索引顺序怎么定
│  │  └─ D2.2 慢在连接池怎么办
│  ├─ R1 缓存怎样保持正确
│  │  ├─ R1.1 删缓存仍可能出现旧值吗
│  │  └─ R1.2 击穿与故障降级
│  │     └─ R1.2.1 Lua 原子性等于持久性吗
│  └─ H1 请求超时是否可以重试
│     ├─ H1.1 如何定位慢在网络还是服务
│     ├─ H1.2 SSE 与普通 HTTP 有何不同
│     └─ H1.3 CORS 能不能阻止 CSRF
├─ S1 如果请求量增长十倍怎么办
│  ├─ S1.1 如何估算容量
│  ├─ S1.2 何时拆微服务或换存储
│  └─ S1.3 重试会不会拖垮系统
└─ O1 怎样把当前代码交付为可运行服务
   ├─ O1.1 CI 应该验证什么
   ├─ O1.2 发布与迁移怎样回退
   └─ O1.3 发生事故先做什么

编号前缀只在本章内使用;项目章节有自己的编号体系。

3 开场与贡献核验

M1 请用一分钟介绍自己

参考表达:

我叫郭家豪,是哈尔滨工业大学威海校区计算机科学与技术专业本科生,预计 2027 年毕业,求职方向是 Go 后端。我主要做过身份认证、异步工作流和日志采集项目,也参与过 Agent 相关实践。我比较关注请求或任务遇到超时、进程退出和并发操作时,系统的状态是否还能正确恢复。今天我想重点介绍【选择一个项目】,其中最有代表性的问题是【一个具体故障场景】,我可以从实现、验证方法和当前局限三个方面展开。

最后两个位置必须自行选择,不要开场就连续解释三个系统。不要在自我介绍里把校企实践关系说成未经确认的直接雇佣关系。

M1.1 你为什么选这个项目讲

**触发:**候选人主动指定主项目。

**参考回答:**因为我能完整说明它的输入、状态变更、持久化、失败恢复和对外结果,并能指出自己实际负责的代码。普通后端面试可以选择 Flostra 的执行链路;认证岗位可以选择 Nyauth;希望展示持久化与 Go 编码细节时可以选择 Gline。最终应按熟悉程度选择,项目功能多并不自动意味着更适合主讲。

M1.2 为什么选 Go 后端 而你的实习还写 TypeScript 和 Python

**触发:**面试官核验岗位动机。

**参考回答:**我选择的是后端系统问题这个方向。Go 的类型系统、标准库和并发工具适合构建网络服务,构建部署也比较直接。Agent SDK、执行生态或现有团队技术栈可能更适合 TypeScript/Python,我愿意按任务选择语言。Go 也有 GC、共享状态竞态、错误处理和依赖运维成本,不能用“天然高并发”代替设计。

个人偏好、学习经历和求职动机请用真实内容补充;这里提供的是合理的技术论证。

M2 项目中哪些东西是你自己完成的

**参考回答结构:**先区分原有框架、本人新增设计、共同开发、AI 辅助和本人未参与部分,再选一个模块说明“原问题 → 决策 → 实现 → 验证”。文件数量、提交数量和仓库归属都不能单独证明设计归属。

可填模板:“在【时间】开始时项目已有【基础】。我负责【模块】,独立决定了【可核实决策】,与【真实角色】协作完成【边界】。目前能展示【代码、测试或报告】;【另一部分】由其他人或 AI 主导,我只参与了【实际范围】。”

M2.1 如果主要代码由 AI 生成 这个项目还有什么意义

**触发:**候选人提到 AI,或面试官发现项目跨度与完成时间较紧。

**参考回答:**AI 使用情况应该直接说明。评价重点是我是否能定义约束、审查生成代码、验证边界、发现错误并承担维护责任。我可以现场解释一条请求如何改变状态,也能说明什么情况下现有实现会失效。若某模块主要由 AI 生成、我还不能独立维护,就应降低对该模块的熟练度表述。

不要回答“AI 只是写一些重复代码”,除非事实确实如此。

M2.1.1 那现在把单实例方案改成两个实例 你先改哪里

**触发:**候选人声称能独立维护。

**参考回答:**先找进程内状态是否承担跨实例正确性,例如 token 消费、幂等键、调度归属、在线订阅和限流。必须共享的状态放到有明确一致性语义的存储;缓存和广播则说明失效与重放策略。然后选一个并发反例,证明两个实例不会同时成功认领同一状态。不能把 Go Mutex 原样当成分布式锁,也不能为了多实例把所有局部变量都搬进 Redis。

M2.2 你说验证过 到底验证了什么

**触发:**出现“端到端”“稳定”“可靠”“无重复”等词。

**参考回答:**说明代码版本、环境、故障注入点、输入集合、预期结果和实际观测。例如服务端提交后故意丢弃响应,再重发同一批次,检查记录数与 ACK 内容。把“测试文件存在”“以前跑过”“当前版本这次通过”“线上长期运行”分开。没有生产压测就直说,不用演示项目指标冒充线上指标。

4 Go 基础追问

G1 服务退出时 如何管理所有 goroutine

**参考回答:**每个后台任务都应有明确所有者、取消信号和完成通知。停止接收新任务后,按业务约定处理已接收任务:可以完成当前事务、持久化未发送数据,或者取消未开始的任务。等待所有相关 goroutine 退出后,再关闭它们共享的数据库和客户端。退出有总期限,超过期限要记录哪些工作未完成,依赖下次恢复。

这是设计答案;能否说项目“已做到”,必须看项目章节的实际代码。

G1.1 调用 cancel 就能保证 goroutine 立即退出吗

**触发:**回答中出现 context 取消。

**参考回答:**不能。context 传递取消信号,代码或依赖需要主动观察。channel 收发用 select 监听取消;数据库与 HTTP 使用支持 context 的接口;CPU 长循环设置检查点。CancelFunc 也不等待后台任务退出,仍需要 WaitGroup 或结果通道。参考 context 文档

G1.1.1 第三方函数永远阻塞且不支持取消 怎么办

**触发:**面试官继续验证对取消边界的理解。

**参考回答:**额外包一个 goroutine 再超时返回,只是放弃等待,阻塞工作仍可能泄漏。优先选能设超时、关闭底层连接或接受 context 的依赖;确实需要强制终止的不可信任务可放到独立进程并管理进程树。对于外部操作,还需考虑进程被终止时是否已经产生副作用。

G1.2 channel 应该由谁关闭 多个 Worker 会怎样

**触发:**候选人用 channel 组织 Worker。

**参考回答:**由能证明不会再有发送者的一方关闭。典型工作池中生产者关闭 jobs;多个 Worker 发 results 时,由等待所有 Worker 结束的协调者关闭 results。接收者随意关闭可能导致发送方 panic。关闭不是垃圾回收的必要条件;它主要表达“不再有新值”。带缓冲 channel 关闭后仍可读出已缓冲数据,应检查 v, ok := <-ch。参考 Go Pipeline 取消模式

G2 两个 goroutine 访问一个 map 怎样保证安全

**参考回答:**只读且已安全发布的 map 可并发读取;有并发写时要建立同步。常用 Mutex 保护状态和相关不变量,或把状态交给单个 goroutine 所有。sync.Map 有适用场景,但不会自动维护跨键不变量。不能靠没有发生 panic 来证明安全。

G2.1 Go 提倡 channel 是否所有地方都应该用 channel

**触发:**候选人把 channel 当作默认答案。

**参考回答:**任务交接、流式管道和明确的状态所有权适合 channel;小范围共享数据保护常用 Mutex 更直接。选择取决于生命周期、等待与取消是否清晰,而不是口号。锁内尽量不做网络调用,避免无界等待放大竞争;但是否拆锁要以数据不变量为依据。

G2.2 go test race 通过是否就没有并发错误

**触发:**候选人以测试通过作保证。

**参考回答:**Race detector 是动态检测,只能覆盖实际执行的路径与时序;它不证明所有执行都无竞争,也不证明不会死锁、泄漏或重复处理。需要结合同步关系和关键场景验证。参考 Go Race Detector

G2.2.1 已经每次读写都加锁 为什么库存仍超卖

**触发:**继续区分数据竞争与业务竞态。

**参考回答:**单次读取与单次写入安全,不代表“检查余额后扣减”这个整体原子。若读完释放锁,再决定扣减,两个 goroutine 都可能看到足够库存。应把同一不变量的检查与修改放入一个临界区,或者用数据库条件更新/CAS。Go 内存模型中的同步保证可见性,业务规则还需要自行定义原子边界。参考 Go 内存模型

G3 goroutine 很轻量 为什么不为每条日志无限启动一个

**参考回答:**轻量不代表免费。goroutine 有栈和调度成本,还会持有对象、连接、缓冲区和请求上下文。系统瓶颈通常在 CPU、磁盘或下游容量;无限启动只是把等待转成内存和连接压力。应根据资源设并发上限、队列上限和过载策略,再用观测确认瓶颈。

G3.1 简述 GMP GOMAXPROCS 是 goroutine 数量上限吗

**触发:**候选人解释调度。

**参考回答:**G 表示 goroutine,M 表示操作系统线程,P 表示运行 Go 代码所需的调度资源。GOMAXPROCS 限制能同时执行 Go 代码的 P 数量,不是 goroutine 总数或所有线程总数上限。网络阻塞和系统调用的处理也不同,不能直接说“每个阻塞 goroutine 占一个线程”。现代 Go 的容器默认值会考虑 CPU 约束,应检查版本和实际配置。参考 Go 容器感知 GOMAXPROCS

G3.2 Go 服务内存持续增长 怎么排查

**触发:**面试官从调度切到运行问题。

**参考回答:**先区分 RSS、Go heap、存活对象和累计分配量,再看 goroutine 数、队列长度与请求负载。持续比较 heap profile 的 inuse_spacealloc_space,结合 goroutine profile 判断是保留引用、无界缓存、切片引用大底层数组,还是阻塞任务持有请求。不要只看到 RSS 不下降就断言泄漏。参考 Go Diagnostics

G3.2.1 可以直接调大 GOGC 或用 sync.Pool 吗

**触发:**候选人开始优化。

**参考回答:**先定位分配来源。调大 GOGC 往往用更多内存换较少 GC 工作;内存紧张时要结合运行时内存限制和真实存活集,不能靠参数修复无限增长。Pool 适合可重建的临时对象,不是可靠缓存,也不能保存依赖存续的资源。复用带敏感内容的 buffer 需要清理;更要避免另一 goroutine 还在读就归还池。参考 Go GC 指南sync.Pool 文档

G4 有一个 error 看起来是 nil 却进入了错误分支 为什么

**参考回答:**接口为 nil 需要动态类型和值都不存在。若 var p *MyError = nil; var err error = p,err 带有动态类型 *MyError,因此不等于 nil。接口返回处没有错误应显式返回 nil,避免把 nil 指针包装成非 nil 接口。

G4.1 为什么面试官还会问这个函数会不会 panic

**触发:**候选人正确识别 typed nil。

**参考回答:**调用接口方法是否 panic 取决于方法是否能处理 nil 接收者,不能从“接口非 nil”直接断言安全。更好的修复是维护清楚的返回契约,而不是在所有调用点加反射判断。参考 Go 语言规范中的接口值

G4.2 把 slice 赋给另一个变量 能否交给另一个 goroutine 随便改

**触发:**面试官切换到 Go 值语义。

**参考回答:**赋值复制切片描述符,通常仍共享底层数组;元素修改会互相影响,append 是否换数组取决于容量。并发场景应明确所有权,必要时复制底层数据。三下标切片限制容量可以避免部分 append 复用,却不隔离原有元素。还要注意复制包含指针的元素并不等于递归深拷贝。参考 Go 切片说明

G4.3 业务错误怎样传递 什么时候用 panic

**触发:**面试官回到服务编码。

**参考回答:**可预期失败作为 error 返回,边界处分类为合适的 HTTP/任务结果。包装时用 %w 保留错误链,调用方以 errors.Is/As 判断稳定语义,避免比较字符串。panic 不作为普通输入错误或数据库暂时失败的控制流;恢复边界用于防止单次任务异常击穿进程,但必须记录失败并清理资源。recover 不能从一个 goroutine 捕获另一个 goroutine 的 panic。参考 Go 错误包装

**可选版本追问:**循环里启动 goroutine 总会捕获同一个迭代变量吗?不总是。Go 1.22 起对循环中新声明的迭代变量引入每次迭代独立变量语义;外部已有变量的赋值、共享引用和数据竞争仍需单独判断。参考 Go 1.22 说明

5 数据库与缓存追问

D1 数据库有事务 为什么仍可能超卖或重复执行

**参考回答:**事务保证什么取决于操作范围、隔离级别和约束。两个事务都读到可用额度再各自写入,可能违反业务不变量;把网络调用放进事务也不会让外部系统自动参与原子提交。应把本库的约束放进条件更新、唯一约束或合适的锁,并单独设计跨系统交付。

D1.1 写一条不会把库存减成负数的 SQL

**触发:**要求把抽象原子性变成实际操作。

**参考回答:**先校验 quantity > 0,再使用带资源与租户条件的单条更新,并检查影响行数。下面是教学示例,不是三个仓库的原表结构。

UPDATE inventory
SET available = available - $3
WHERE tenant_id = $1
  AND sku_id = $2
  AND available >= $3
RETURNING available;

没有返回行不一定只是库存不足,也可能资源不存在或不属于租户,应按 API 信息暴露策略处理。CHECK (available >= 0) 可作为数据库最后一层保护,但不能替代业务错误分类。多个资源的复杂不变量可考虑事务内锁行,统一锁顺序降低死锁风险。

D1.2 PostgreSQL 的 Read Committed 和 Repeatable Read 有什么区别

**触发:**候选人提到隔离级别。

**参考回答:**Read Committed 通常每条语句建立可见性快照,因此同一事务两次查询可能看到新提交结果。PostgreSQL 的 Repeatable Read 使用事务级快照,能避免其定义下的幻读,但仍可能有序列化异常,例如基于多行条件的写偏差。Serializable 提供更强保证,但可能拒绝一个事务,需要应用重试。参考 PostgreSQL 事务隔离

D1.2.1 序列化失败直接重试最后一条 UPDATE 行不行

**触发:**候选人说 Serializable 失败会重试。

**参考回答:**一般应重跑整个事务及其依赖读取,使决策基于新快照;已经失败的事务也需要结束后再开启。重试有上限、退避和取消。不要把已发送邮件、支付等不可回滚副作用无保护地放在可能整体重试的代码块里。参考 PostgreSQL 序列化失败处理

D2 日志分页接口慢 你先做什么

**参考回答:**先固定慢查询条件与数据分布,分解连接等待、数据库执行和结果传输。检查时间范围、租户过滤、排序与 limit 是否有界,再用执行计划观察实际扫描量、排序、估算偏差、缓存命中和磁盘读取。EXPLAIN ANALYZE 会执行语句,尤其对写操作不能当成无副作用查看。参考 PostgreSQL EXPLAIN

D2.1 为什么日志查询常用 project_id occurred_at id 联合索引

**触发:**面试官要求说明索引与查询匹配。

**参考回答:**在项目等值过滤、按时间与唯一 ID 排序的查询中,先缩小项目范围,再沿排序键做范围扫描,适合 keyset 翻页。下一页用 (occurred_at, id) 与上页末行比较,ID 负责时间相同情况下的稳定顺序。字段顺序应按实际 WHERE/ORDER BY 决定,不是“选择性最大永远放第一”。不同数据库版本对跳跃扫描等优化支持不同,最终看计划。参考 PostgreSQL 多列索引

D2.2 SQL 很快 接口还是慢 可能是什么

**触发:**候选人只想加索引。

**参考回答:**可能在等连接池,也可能锁等待、序列化 JSON、网络写出或下游调用慢。sql.DB 是并发安全的连接池句柄,不应每个请求创建一份。设置并观测最大连接数、空闲连接、生命周期、等待次数与等待时长;及时关闭 Rows 和事务。把池无限调大可能把数据库压垮。参考 Go 数据库连接管理

R1 更新数据库以后 怎样处理缓存

**参考回答:**先根据业务确定允许多长的旧数据窗口。普通 cache-aside 常先提交数据库,再失效缓存;但它不天然提供严格一致性。认证撤销或唯一消费这种安全状态不应直接套普通业务缓存的“短暂旧值可接受”。此外要区分 Redis 在系统里是可重建缓存,还是承担正确性的状态存储。

R1.1 先更新数据库再删缓存 为什么仍会有旧值

**触发:**候选人称此方案完全一致。

**参考回答:**读请求在更新前读到旧数据库值、写请求提交并删缓存、旧读请求随后把旧值写回,就出现旧缓存复活。删除失败也会留下旧值。可按需求使用短 TTL、版本化写入、可靠失效事件或更严格的读写协议。延时双删不能无条件证明消除所有竞态,因为无法为所有请求延迟设可靠上界。

R1.2 热点缓存失效或者 Redis 故障 怎么保护数据库

**触发:**从一致性转向可用性。

**参考回答:**TTL 加抖动避免集中过期,对同键重建做请求合并,对合法不存在结果做有界负缓存,并对数据库访问限流。多实例时进程内 singleflight 只合并本实例请求。Redis 故障的降级按业务分类:普通展示数据可尝试数据库回源;token 一次性消费不能退回不安全的“本机算了就通过”。

R1.2.1 Redis Lua 原子执行 等于故障切换后状态不丢吗

**触发:**候选人用 Lua 解释正确性。

**参考回答:**Lua 解决执行期间的原子检查与修改,不自动保证持久化和副本切换语义。还要看 RDB/AOF、刷盘策略、复制、故障转移与键的 TTL。脚本运行期间会阻塞其他命令执行,因此不能写无界循环或处理过大数据。参考 Redis Lua 原子执行Redis 持久化

6 网络与接口追问

H1 客户端请求超时 能不能直接重试

**参考回答:**超时说明客户端没有及时得到结果,不能证明服务端没有提交。读操作通常更适合重试;写操作先定义幂等键、重复请求内容冲突处理和结果查询方式。重试要区分暂时网络错误、限流、服务故障和永久业务错误,并设预算、退避与抖动。HTTP 方法的幂等语义也不意味着每个具体业务实现都遵守了它。参考 HTTP 语义中的幂等方法

H1.1 一次请求经过哪些阶段 怎么知道慢在哪里

**触发:**候选人说“网络问题”。

**参考回答:**依次区分 DNS、TCP 建连、TLS、发送请求、等待响应头、读取响应体;连接复用时前几步可能不发生。Go 可以用 httptrace 配合服务端 trace 和访问日志。服务端有请求且已提交、客户端读取超时,与根本没到达服务端是不同故障。通过 request ID 关联,但不能记录完整 token、密码或敏感正文。参考 Go httptrace

H1.2 SSE 为什么可能被代理缓存 优雅退出又有什么特殊性

**触发:**简历出现 SSE。

**参考回答:**SSE 是长时间维持的 HTTP 响应,服务端按事件格式输出并 flush;中间代理缓冲、空闲超时或客户端不持续读取,都可能导致延迟或断连。心跳与代理配置要配合。SSE handler 本身也要监听退出信号,不能只调用 Server.Shutdown 后假设所有长连接马上结束。标准 EventSource 通过 idLast-Event-ID 支持重连位置传递,但可靠恢复还依赖服务端保存与重放事件;项目若用自定义 fetch 流或 sequence 参数,应按实际协议解释。参考 SSE 标准Go HTTP Shutdown

H1.3 CORS 配好了 为什么还需要考虑 CSRF

**触发:**面试官核验 Web 安全基础。

**参考回答:**CORS 主要约束浏览器跨源脚本读取响应的权限,不是通用的请求发送防火墙。Cookie 会随某些跨站请求自动带上,所以需要结合 SameSite、CSRF token、Origin/Referer 校验和接口设计。Bearer token 不会像 Cookie 一样被浏览器自动附加,但会有存储和 XSS 风险。OAuth 的 state、PKCE、OIDC nonce 各有绑定目标,不应相互混为一个机制。参考 OWASP CSRF 防护指引

7 系统设计与交付追问

S1 如果项目请求量增长十倍 你怎么扩展

**参考回答:**先问当前量、突发比、数据大小、延迟目标、失败预算和瓶颈在哪里。“十倍”不能直接推出微服务或分库。先对无状态 API、后台消费、数据库写入和查询分别测量,再选择增加实例、调整批次、限流、索引或存储分层。以下数字都是面试假设,不是项目业绩。

S1.1 假设日志每秒两万条 每条平均一千字节 怎么估算

**触发:**面试官要求量化。

**参考回答:**原始流量约 20 MB/s,持续一天约 1.728 TB,尚未包含协议、索引、副本、WAL 和备份。一天保留与三十天保留会导向完全不同的成本。若下游断开十分钟,原始积压约 12 GB,必须明确 Agent 磁盘预算和磁盘满策略。估算后用代表性负载验证压缩比与实际写放大,不用理论值直接宣布容量满足。

S1.2 什么时候换 ClickHouse 或拆微服务

**触发:**面试官改变规模或组织约束。

**参考回答:**当日志分析以大范围聚合为主、现有 PostgreSQL 成本或延迟无法满足目标时,才评估列式存储;同时设计写入、查询、保留和一致性契约。微服务适用于明确的独立部署、伸缩、故障或团队边界,不为技术栈数量而拆。眼下身份中心或小型日志平台保持模块化单体可能更容易维护。

S1.3 全部请求失败后一起重试 会发生什么

**触发:**候选人提出重试恢复。

**参考回答:**会形成重试放大,层层重试还可能乘法增长。应明确由哪层重试,限制次数和总时长,使用指数退避与抖动,设置队列上限,并区分可重试和永久失败。服务恢复时控制积压回放速率,不能让历史任务把新请求完全挤掉。租户配额和公平调度要在有实际需求时加入。

O1 怎样把当前代码交付为可运行服务

**参考回答:**把配置、依赖版本、数据库迁移、构建产物、启动方式、健康检查和回滚步骤组成可复现交付。Docker Compose 可以固定开发拓扑,但不等于高可用或生产验收。机密通过环境或受控配置注入;镜像不应带开发凭据,日志不能泄漏它们。

O1.1 CI 应该验证什么

**触发:**候选人列出 GitHub Actions。

**参考回答:**检查编译、静态问题、关键行为和跨进程协议。对 Go 跑适当测试和 vet;有共享并发状态时跑 race;数据库约束、Lua 和消息确认用真实依赖验证。前后端构建和契约检查分别覆盖自己的边界。优先保护 token 一次性消费、事务 ACK、旧 attempt 拒绝和重放边界,不追求对每个 getter 都写测试。

O1.2 代码能回滚 数据库变更也能直接回滚吗

**触发:**候选人说发布失败恢复旧镜像。

**参考回答:**不一定。删除列、改变语义和不可逆数据迁移可能让旧版本无法运行。优先使用兼容的扩展、数据回填、切换和收缩步骤,验证新旧版本共存窗口。备份只是前提,还要知道恢复时间和恢复点;不能以未经演练的备份承诺恢复能力。

O1.3 线上积压突然增长 你先做什么

**触发:**面试官进入故障处理。

**参考回答:**先确认影响范围和增长开始时间,区分生产速率增加还是消费吞吐下降,检查错误率、延迟、数据库锁/连接、消息未 ACK、Worker 心跳、磁盘与最近发布。优先控制影响,例如限流或回退可疑发布;保留故障证据后再修根因。不要一开始就清空队列、重置数据库或删除 WAL。

8 面试官如何根据表现改变难度

表现 下一步 评价意义
能讲完整正常链路 问提交前后各一个崩溃点 检查是否理解状态边界
能讲失败窗口 加上第二个实例、重试或响应丢失 检查并发与幂等是否独立成立
能讲正确性 限定更低规模、更少组件 检查是否会过度设计
能讲设计但写不出 SQL/Go 转入小型手写题并要求测试 检查是否能独立落地
回答使用绝对保证 要求构造反例 检查诚实性与技术精度
不知道但能明确边界 给条件,看能否推导 校招可以接受知识空白,推理过程有价值

练习时每次只沿一条路径连续追问 3–4 层。每个回答先用一句话给结论,再用一个时间线、例子或代码证明;不要把整章当背诵稿。