面灵AI→

极智嘉一面:Go 后端八股与机器人仿真方向

时间
2026-10
来源
牛客网

《面试题目》

  1. 个人介绍。
  2. 项目介绍。
  3. 你使用的乐观锁是如何设计的?版本号如何定义,发生并发写冲突时如何重试?
  4. 为什么选择 ClickHouse,而不是传统的 MySQL 事务型数据库?
  5. 进程和线程有什么区别?它们在资源拥有、操作系统调度、故障隔离和使用场景方面有何不同?
  6. 哪些场景更适合多进程,哪些场景更适合多线程?
  7. 多线程同时访问同一个变量可能出现哪些问题?
  8. 一个 Web 服务的内存持续增长,可能有哪些原因?
  9. TCP/IP 四层模型分别包含哪些层?每层的主要职责和典型协议是什么?
  10. Go Channel 的底层数据结构是什么?
  11. Channel 阻塞时,Goroutine 如何进入等待队列,又会在什么情况下被唤醒?
  12. 介绍一下 Go 的 GMP 调度模型。G、M、P 分别是什么?
  13. 大量缓存同时失效并导致请求打到数据库时,应如何处理?
  14. 你是否做过前端开发?了解哪些前端性能优化手段?
  15. 多个用户频繁查看大批量流量数据时,如何通过前端限流、短 TTL 缓存或防抖降低后端压力?
  16. 系统中存在大量离散的二维坐标点。给定左上角和右下角两个点,如何快速统计矩形范围内的数据点数量?
  17. 如何使用 HashMap 和双向链表实现一个时间复杂度为 O(1) 的 LRU Cache?get 和 put 分别如何操作?
  18. 如果 LRU 中每个节点还带有独立 TTL,应如何处理过期节点?
  19. 当 LRU 容量已满时,应优先清理过期节点还是按 LRU 淘汰?
  20. 你对极智嘉及其业务有什么了解?
  21. 你如何理解机器人仿真平台?
  22. 你过去是否接触过仿真相关工作?
  23. 你对具身机器人方向的了解程度如何?
  24. 你的 Linux 使用经验如何?日常是否在 Linux 上部署过 Nginx、Docker、Kubernetes 等服务?
  25. 使用过哪些 CI/CD 工具或框架?是否有自行搭建 CI/CD 流程的经历?
  26. 介绍一个你做过的最困难、最有挑战的项目。
  27. 反问:机器人仿真岗位具体需要负责哪些工作?
  28. 反问:仿真工作目前可以怎样与 AI 或 Agent 结合,以降低人工工作量、提升研发效率?公司内部是否已有相关尝试?

《参考解析》

乐观锁的关键不是版本号本身,而是冲突之后的处理策略。 常见实现是给行加一个 version 字段,读取时带出版本,更新时写成 UPDATE ... SET version = version + 1 WHERE id = ? AND version = ?,受影响行数为 0 就说明期间被别人改过。设计上要讲清三件事:版本号的粒度(整行一个版本,还是按字段/业务维度分版本,后者并发度更高但更复杂);重试策略(有限次重试加指数退避与随机抖动,避免固定间隔把并发重新对齐;重试前要重新读取最新数据,不能拿旧快照直接覆盖);以及退避到什么时候放弃并向上返回冲突(写冲突频繁说明该换成悲观锁、串行化队列或状态机式的原子操作)。追问常落在与悲观锁的取舍上:读多写少、冲突率低时乐观锁省锁开销;冲突率高时重试会放大流量,反而不如 SELECT ... FOR UPDATE 或数据库的唯一约束兜底。

选 ClickHouse 的理由要落在查询模式上,而不是「它更快」。 它是列式存储加向量化执行,同一列的相似数据连续存放、压缩率高,扫描时只读涉及到的列,所以对「大宽表、按时间范围聚合、扫很多行但只取少数几列」的分析型查询非常合适;MergeTree 系列引擎支持按分区与排序键做稀疏主键索引、后台合并、物化视图预聚合,这些都是为吞吐设计的。代价同样明确:不适合高频单行更新与删除(变更以异步 mutation 形式落地,代价大)、不支持完整事务与高并发点查,join 大表要格外小心。所以典型的取舍是 OLTP 走 MySQL 保证事务与一致性,分析类与漏斗、报表类查询把数据同步到 ClickHouse 承担;两者之间用 Binlog 订阅或批量写入同步,并接受最终一致。

进程与线程的区别按四个维度答:资源拥有、调度、隔离、场景。 进程有独立地址空间与文件描述符表,线程共享进程的地址空间和资源、只有自己的栈和寄存器上下文,所以线程创建与切换成本低但一个线程崩溃会拖垮整个进程。Linux 里两者都由内核调度、都对应 task,差别主要在共享资源上。多进程适合需要故障隔离、能用多核并行且通信量不大的场景(比如按 CPU 密集计算分片、需要独立崩溃恢复的 worker),多线程适合共享大量数据、通信频繁、需要低切换开销的场景(IO 密集的服务用线程池或协程承载并发)。多线程同时访问共享变量会带来可见性、原子性与有序性问题:CPU 缓存导致读到旧值(需要内存屏障或原子类型),i++ 这类复合操作存在竞态(需要锁或 CAS),编译器与 CPU 重排序会破坏依赖假设(需要 happens-before 保证),实践中还要注意伪共享导致的性能退化。

「内存持续增长」这类开放题按分类排查,比猜原因得分高。 先分是泄漏还是正常的缓存膨胀:看堆 dump 里对象数量的增长曲线,比较不同时间的支配树,能持续增长且不回收的对象就是嫌疑。常见几类:集合类只增不减(本地缓存没有上限与淘汰、监听器/回调注册没有注销)、未关闭的资源(连接、文件描述符、线程池任务队列积压)、大对象生命周期被意外延长(ThreadLocal 在长生命周期线程里、静态字段持有请求级数据)、元空间或 native 内存(类加载器泄漏、直接内存 ByteBuffer 未释放)、以及查询把大量数据一次性读进内存。排查手段是堆 dump 加 MAT 分析、按对象数量排序、看 GC 日志中老年代占用是否逐轮抬升。修的方向是给所有缓存设上限与 TTL、所有注册都有配对的反注册、把批量处理改成分页或流式。

Go 的 Channel 与 GMP 调度,答出结构就能延伸出所有追问。 Channel 底层是 hchan:一把互斥锁、环形缓冲数组(buf、sendx / recvx 下标)、等待队列 sendq / recvq、以及 closed 标志和元素大小信息。读一个空 channel 或写一个满 channel 时,当前 goroutine 会被包装成 sudog 挂进对应等待队列并让出 P;对面完成操作或 close 时,运行时会取出等待者,直接把数据交给它(有缓冲的还会做一次内存拷贝)并把它放回可运行队列。GMP 里 G 是 goroutine、M 是内核线程、P 是调度器给的上下文(含本地运行队列、内存缓存与状态),M 必须绑定 P 才能执行 G。调度要点:本地队列优先、本地空时从全局队列或其他 P 偷一半(work stealing)、G 发起阻塞系统调用时 M 与 P 解绑并把 P 交给其他 M(handoff),网络轮询器与 sysmon 负责把就绪的 G 放回队列并做抢占。结合 1.14 之后的异步抢占,可以顺带答「死循环的 goroutine 为什么能被调度走」。

缓存同时失效要按「不让流量同时打到数据库」来设计。 常规手段分四层:过期时间加随机抖动,避免同一时刻批量失效;热点数据逻辑过期加异步刷新,读到的旧值先返回、后台单飞刷新(single flight 合并同一 key 的并发回源);回源加互斥锁或分布式锁,未拿到锁的请求短暂等待或降级返回;再往上做多层缓存(本地 + Redis)与限流熔断兜底,数据库前面加队列缓冲。另外要区分它是缓存雪崩(大量 key 同时过期)还是缓存击穿(单个热 key 过期)还是穿透(查不存在的 key),三者对策不同:雪崩靠打散与预热,击穿靠单飞与永不过期,穿透靠空值缓存与布隆过滤器。生产上还应准备好「缓存整体不可用」的降级预案,把非核心接口直接关掉。

前端性能与降后端压力的问题,答题时要把手段和代价一起讲。 前端侧的核心是减少请求次数与传输体量:输入类查询做防抖(等待用户停下再发)与节流(限频)、相同请求合并与结果缓存、分页或虚拟滚动避免一次渲染上万行、静态资源压缩与懒加载、用 Web Worker 把大数据计算移出主线程。但要注意防抖只能减少请求数,挡不住单次请求本身很重,所以后端侧还要配合:短 TTL 缓存加 key 维度合并、把聚合计算预计算成物化视图或宽表、按时间粒度降采样返回、大结果改成游标分页或流式返回、以及把高频查询的配额与限流放在网关。真正严谨的答法是先给量级(多少用户、每秒多少次查询、单次返回多大),再说明哪种手段解决的是哪个瓶颈——只说「加缓存、加防抖」会被追问到底是哪一层在扛。

两道数据结构题考的是「能不能按数据规模和动态性选方案」。 LRU Cache 的标准实现是 map[key]*node 加一条双向链表:get 命中后把节点摘下来挂到表头并返回值,put 时命中就更新值并提到表头,未命中则新建插表头,超容量就把尾部节点摘掉并从 map 里删掉,两个操作都是 O(1)。加上 TTL 之后,过期判断变成「每次访问时惰性检查 + 后台定期清理」,因为链表是按访问顺序而不是按过期时间排的,扫链表找过期节点是 O(n),所以更常见的做法是另建一个按到期时间排序的结构(最小堆或时间轮)来驱动清理。容量满时优先清过期还是按 LRU 淘汰,答案取决于语义:过期节点占着位置但已经无效,先清过期能回收更多有效容量;但如果过期比例很低,扫堆的开销就不划算,可以退化成一个延迟删除队列,在 put 找不到可淘汰对象时再从堆里捞。回答时点出「惰性删除 + 定期删除」这个组合以及内存回收的时机,比只背链表操作更完整。矩形范围计数题同理:点集静态时按 x 排序加前缀和(或按 y 建可持久化线段树、二维 BIT、KD 树)都能做到 O(log n) 单次查询;如果点会动态增删,就换成 KD 树或分层网格分桶,并说明选型依据是数据量、查询次数与更新频率的组合。