蔚来 Golang后端开发 一面

面试基本信息

  • 公司: 蔚来汽车(新能源汽车独角兽)
  • 职位: Golang后端开发工程师
  • 面试轮次: 一面(技术面)
  • 面试形式: 线上视频面试
  • 面试时长: 约60分钟(包含15-20分钟代码题)
  • 面试官: 技术leader,主要负责车辆测试系统开发

面试问题与回答

问题1:自我介绍和项目经历

面试官问题

“你先做个自我介绍,然后介绍一下你的项目经历。”

我的回答

"好的,首先我叫王宇哲,来自华东师范大学,现在是在软件工程学院的软件工程专业就读,目前是研一。研一的话我们导师没有说不同意出来实习,也没有说同意,但是我因为论文基本上已经完成得差不多了,所以也跟导师商量好了,科研上的任务基本上没有太多了,所以现在就来找实习了。

然后我主要的技术栈是Go语言,以前也做过Java、PHP相关的项目。现在主要技术栈的话,最近做的是抖音商城,它是一个参加字节跳动青训营的项目。这个项目用gin加gRPC,然后融合了一些可观测性之类的组件进去,服务注册服务发现,基本上能想到的跟微服务相关的常见的东西基本上都融入进去了。

另一个项目的话叫ani q,是我自己学习之余做的一个项目,是去年暑假的时候还没开学的时候做的。它是一个基于字节跳动那个框架搭建出来的微服务架构的多代理AI系统。以前本科的话拿过国家奖学金,也参加过蓝桥杯拿过国一,计算机设计大赛拿过国三,还有大学生数学竞赛拿过一等奖,差不多就这些。"

面试官后续提问

“你那个AI是做了一个什么样的具体的方向?AI那个的话能详细讲一下吗?”

我的回答

"AI那个的话它主要就是除了微服务,除了一些简单基本的微服务以外,我们主要当时设想是这样一个AI能够去就比如说它会有一个架构,它是首先一个主AI是用来分析用户需求的。然后它会类似于一个树形的结构,它会分配给下级的AI去执行一些执行那个微服务的,执行执行决定要转发给哪个微服务。

就比如说它可能有订单微服务,就比如说一些常见的商城系统,它可能有订单微服务,用户微服务,然后它跟AI提出一个请求,它会然后主AI它会去判断应该是交给哪个微服务去操作的。然后交给那它不会直接交给那个微服务,会交给一个下游AI,那个下游AI的话它是知道那个微服务的所有的接口的。然后第二级AI的话,它可以去决定去调用哪一个函数。这样的话它就可以以一种树形的结构去调度的话,它可以它因为现在AI上下文的长度不是很高。因为上下文一旦多了以后,它这个准确度就会下来。如果用这种架构的话,它即使是即使它支持上下文有限,它也可以达到一个很好的效果。因为有点像分治的思想在里面。"

面试官问题

“第一个项目应该没有很复杂的业务,在里面你是自己练练手的一个项目,对吧?主要就是一些商城的一些常用的一些模型的一些CRUD为主对吧?消息队列是用来做什么?能详细说说吗?比如说你是处理什么样的业务需要放到消息队列里面。”

我的回答

"不只是CRUD,它主要是因为要保证一些可观测性之类的,所以说它融合挺多。比如说消息队列也融合进去了。消息队列主要是为了能够保证高并发场景下的那些负载,就是服务对高并发场景下,它服务器负载就是单个节点的负载不会太大。

比如说订单,比如说创建订单,你都不是立即处理,都是异步处理的。这个放到消息队列,这样的话就是服务器压力不会太大。我们用的RocketMQ。"

标准/建议回答

自我介绍这块我觉得可以更有条理一些。可以这样组织:

"大家好,我是xxx,目前在华东师范大学软件工程专业读研一。技术方面,我主要专注于Go语言后端开发,有约1年的实践经验。

项目经历方面,我主要完成了两个比较有代表性的项目:

第一个是字节跳动青训营的抖音商城项目,这是一个完整的微服务架构项目。技术栈使用gin+gRPC,集成了服务注册发现、消息队列RocketMQ、链路追踪Prometheus等组件,主要解决高并发场景下的订单处理问题。

第二个是我个人开发的ani q多代理AI系统,基于字节跳动的Hertz和Kitex框架,设计了分层的AI调度架构,通过解析Proto文件实现泛化调用,是一个比较有创新性的尝试。

学术方面,我获得过国家奖学金,在算法竞赛和数学竞赛中也有不错的成绩。"

总结反思

  • 当时介绍有点啰嗦,逻辑不够清晰
  • 应该突出核心技术能力和项目亮点
  • 可以提前准备1-2分钟的精炼版本

问题2:Go语言defer机制

面试官问题

“你说一下Go语言里面那个defer有什么用?”

我的回答

“defer的话它主要就是它会在一个函数执行前去,这个函数结束之前去执行这个defer里面的那个对一些方法。然后它的话是使用了一个这样的结构,就是它先进先出。也就是说比如说你defer它可能定义的是顺序是1、2、3,但它最后执行的话,执行defer里面的函数,它会用3、2、1的顺序去执行。它这个一般是用在比如说我开启了一个数据库,然后我们要在函数结束的时候要给它销毁掉,执行这样一些方法。”

标准/建议回答

关于defer机制,我觉得可以这样回答会更准确:

"defer是Go语言的一个关键字,主要用于延迟执行函数调用,直到包含它的函数返回时才执行。

defer有几个重要特性:第一,它是先进后出的执行顺序,也就是LIFO(Last In First Out),后定义的defer会先执行;第二,defer的参数会在defer语句执行时就确定值,而不是在实际调用时;第三,即使函数发生panic,defer语句也会执行。

实际应用中,defer主要用于资源清理,比如关闭文件句柄、释放数据库连接、解锁mutex等。这样可以确保资源在函数退出时一定会被正确释放,避免资源泄露。

举个例子:

1
2
3
4
5
6
7
8
9
10
func processFile() error {
file, err := os.Open("test.txt")
if err != nil {
return err
}
defer file.Close() // 确保文件一定会关闭

// 处理文件逻辑...
return nil
}

总结反思

  • 当时说成了"先进先出",应该是"后进先出"(LIFO)
  • 表达有些磕磕绊绊,逻辑不够清晰
  • 没有提到panic时也会执行defer这个重要特性
  • 应该给出具体的代码示例

问题3:Go语言垃圾回收机制

面试官问题

“说一下Go语言的垃圾回收机制是什么样子?”

我的回答

“这个垃圾回收机制的话我没有太了解,但是我记得它好像是有一个叫三色标记法,好像是Go语言以后的。我记得它是会用一个黑色节点去标记,标记了黑色节点以后,它就是标记为这些资源不会再被使用了,就是等待被回收的。然后它不会一次性回收,它会好像是每隔一段时间它会回收一部分。”

标准/建议回答

Go语言的垃圾回收确实是基于三色标记法,我来详细说一下:

"Go语言使用的是并发三色标记清除算法。具体来说:

三色标记法把所有对象分为三种颜色:

  • 白色:未被访问的对象,回收候选
  • 灰色:已被访问但其引用还未被扫描的对象
  • 黑色:已被访问且其引用也已被扫描的对象

回收过程是这样的:首先从根对象开始,标记为灰色;然后扫描灰色对象的所有引用,将引用对象标记为灰色,自己变为黑色;重复这个过程直到没有灰色对象;最后清理所有白色对象。

Go的优化点在于:第一,使用写屏障技术保证并发安全;第二,采用混合写屏障减少STW(Stop The World)时间;第三,通过调优参数GOGC控制回收频率,平衡吞吐量和延迟。

现在Go 1.5+版本的GC延迟已经控制在毫秒级别,对于大部分应用来说性能表现都很好。"

总结反思

  • 当时只记得三色标记法的概念,没有深入理解
  • 应该学习完整的GC流程和优化策略
  • 可以补充一些性能调优的实践经验

问题4:Go协程间通信方式

面试官问题

“Go语言里面goroutine之间有哪些可以通信的方式?举2到3个。”

我的回答

"一般来说的话,第一种就是它用那种信号量机制,比如说sync这个库里面它会提供mutex,用这种方式去执行一个类似于同步之类的操作,这也是一种通信方式。

最主要的还是channel这种,它相当于有点像是一个消息通道。然后左边有一个消费者,右边一个…就是左边一个provider,右边一个consumer,然后就是用这个信号,用这个channel的话,它就可以实现一些同步的操作,还可以传递数据。

还有一个叫WaitGroup,我记得它会就是它就比如说你在开启三个协程的时候,每个协程开启之前,你都给它WaitGroup去加1。然后最后的话就是在协程开启完以后,我们去在主函数里面最后去等待等待这些wait WaitGroup被释放,然后就可以同时等待三个协程结束。这样的话就可以同时等待三个程序结束以后,再继续执行下面的操作。

还有一种是共享内存,共享内存它就是直接可以读取。比如说主函数里面的定义了一个变量,然后协程里面它就可以读取主函数这个变量。"

标准/建议回答

Go协程间通信主要有这几种方式:

"第一种是Channel,这是Go语言推荐的通信方式,符合’不要通过共享内存来通信,而要通过通信来共享内存’的理念。Channel可以是无缓冲的(同步)或有缓冲的(异步),能够安全地在协程间传递数据。

第二种是共享内存配合同步原语,比如使用sync.Mutex、sync.RWMutex来保护共享变量的访问。这种方式性能较好,但需要小心处理竞态条件。

第三种是其他同步原语,比如sync.WaitGroup用于等待一组协程完成,sync.Once用于确保函数只执行一次,sync.Cond用于条件等待等。

实际开发中,优先推荐使用Channel,因为它在编译时就能帮我们检查很多并发安全问题。只有在性能要求特别高的场景下,才考虑使用共享内存的方式。

需要注意的是WaitGroup严格来说不是通信方式,而是同步机制,用于协调协程的执行顺序。"

总结反思

  • 当时把WaitGroup也算作通信方式,概念有点混淆
  • 应该明确区分通信和同步的概念
  • 可以补充一些具体的使用场景和最佳实践

问题5:MySQL B+树索引结构

面试官问题

“MySQL你用的比较多对吧?说一下MySQL的InnoDB引擎的B+树索引,它是什么样一种数据结构?”

我的回答

"B+树的话它一般来说它只在叶子节点去存储真实数据。然后它的非叶子节点的话,它会存储索引的,它会存储一些索引。然后的话它一般来说用这种方法的话,第一它是相当于是B树的改进版本。因为B树的话它每个节点它都会存储一定的数据和索引。但用这种方式的话,它可能树的高度会变会增加得特别快。

然后其次就是它每次IO的话它可能会读取它IO的次数可能会变多。因为它读取每次读取一个节点,它都要进行一次IO,但是用B+树的话,它就是索引都聚集在一块了。然后它读取的IO次数就会减少。然后它IO次数减少了以后,它整体的性能就会,整体的查询速度就会变快。然后B+树它最后一层的话,它还有一个就是它会把每个叶子节点用一个指针给它连起来,这样可以实现一个范围的查找。"

标准/建议回答

关于B+树索引,我觉得回答得还可以,可以再补充一些细节:

"InnoDB使用B+树作为索引结构有几个关键特点:

首先,B+树是B树的改进版本。B树的每个节点都存储键值和数据,而B+树只有叶子节点存储数据,非叶子节点只存储键值用于导航。

这样设计的优势是:第一,非叶子节点可以存储更多的键值,减少了树的高度,从而减少磁盘IO次数;第二,所有数据都在叶子节点,叶子节点之间通过双向链表连接,非常适合范围查询和排序操作;第三,查询路径稳定,任何查询都需要从根节点走到叶子节点,性能可预期。

在InnoDB中,主键索引的叶子节点存储的是完整的行数据,叫做聚簇索引;而辅助索引的叶子节点存储的是主键值,需要回表查询获取完整数据。

一般来说,InnoDB的B+树高度在2-4层,每层能存储的数据量是指数级增长的,所以即使千万级数据也能保持很好的查询性能。"

总结反思

  • 基本概念回答正确,但可以更深入
  • 应该补充聚簇索引和非聚簇索引的区别
  • 可以举一些具体的数据量和性能的例子

问题6:Redis分布式锁

面试官问题

“Redis有用过吗?说一下Redis如果作为分布式锁,它是什么样一个特性可以拿来作为分布式锁?”

我的回答

“Redis分布式锁大概了解过。我记得它是一个,我记得它是一个单命令,什么样那个命令什么样命令是lock吗?setnx,不是,因为我觉得还是没用过。”

标准/建议回答

Redis分布式锁主要利用的是Redis的原子性操作特性:

"Redis可以实现分布式锁主要是因为几个特性:

第一,Redis是单线程模型,命令执行是原子性的,不会出现并发问题。

第二,可以使用SETNX(SET if Not eXists)命令,它只有在key不存在时才会设置成功,这正好符合锁的语义 - 同一时间只有一个客户端能获取到锁。

第三,可以通过EXPIRE设置过期时间,避免死锁问题。

具体实现时,通常使用SET命令的组合参数:

1
SET lock_key unique_value NX PX 30000

这样可以原子性地设置key和过期时间。

释放锁时需要使用Lua脚本来保证原子性:

1
2
3
4
5
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end

当然,Redis分布式锁也有一些局限性,比如在主从架构下可能出现锁丢失的问题,这时候可以考虑使用Redlock算法或者其他分布式锁方案。"

总结反思

  • 当时确实没有实际使用经验,回答很简单
  • 应该至少了解基本的实现原理
  • 可以补充学习一些实际的使用场景

问题7:队列和栈的区别及应用场景

面试官问题

“说一下队列和栈有什么区别,他们分别可以在业务上举个例子,在什么样的场景下去使用。”

我的回答

"队列的话一般是用在这种先进先出的情况。就比如说我刚刚说的那个消息队列,它有一种也是一种队列的数据结构。它这样的话就可以保证先生成的订单被,就是先生成的订单会先处理。

然后栈的话是这样一种先进后出的一种数据结构。然后它一般来说的话就是比如说我们在实行一个递归的操作的时候,它的那个函数其实就是一个用栈的实现来,用这样的方式来实现的。"

标准/建议回答

队列和栈的区别及应用,我觉得当时回答得还可以,可以再丰富一些:

"队列(Queue)是先进先出(FIFO)的数据结构,栈(Stack)是后进先出(LIFO)的数据结构。

队列的典型应用场景:

  • 消息队列:如你提到的订单处理,保证请求按顺序处理
  • 广度优先搜索(BFS):层次遍历树或图
  • 任务调度:操作系统的进程调度,打印机任务队列
  • 缓冲区:生产者消费者模型中的数据缓冲

栈的典型应用场景:

  • 函数调用栈:存储函数调用的上下文信息
  • 深度优先搜索(DFS):递归遍历的模拟
  • 表达式求值:中缀表达式转后缀表达式
  • 浏览器历史记录:后访问的页面先返回
  • 撤销操作:编辑器的Ctrl+Z功能

在实际开发中,比如做一个计算器应用,解析数学表达式时就需要用栈来处理括号匹配和运算符优先级。而做一个任务系统时,通常用队列来保证任务的公平调度。"

总结反思

  • 基本概念回答正确
  • 应用场景可以更丰富一些
  • 可以结合具体的技术实现来说明

问题8:深度优先遍历和广度优先遍历

面试官问题

“说一下深度优先遍历和广度优先遍历分别是什么样的一种遍历方式。”

我的回答

"深度优先遍历的话,我直接在树这个结构,就不在图里面去讲了。比如说在树这个结构下的话,深度优先遍历的话,它会从根节点开始一级级遍历。它每次只会选择一个分支去进行遍历,然后最后碰到了叶子节点以后再返回,不一定是叶子节点,就是碰到了比如说它是先向左遍历,那它碰到没有左节点以后,它会继续向右遍历。然后相当于它就先往深处遍历,然后慢慢再回溯,然后再往深处遍历,是这样一个操作过程。

而广度优先遍历的话它是一层一层的遍历,它就是有种那种比较发散的那种感觉。"

标准/建议回答

关于DFS和BFS,我觉得可以这样解释会更清楚:

"深度优先遍历(DFS)和广度优先遍历(BFS)是两种不同的图/树遍历策略:

深度优先遍历的特点:

  • 尽可能深地访问节点,直到无法继续为止,然后回溯
  • 使用栈(递归或显式栈)来实现
  • 空间复杂度相对较低,为O(h),h是树的高度
  • 适用场景:找路径、检测环、拓扑排序等

广度优先遍历的特点:

  • 逐层访问节点,先访问距离根节点近的节点
  • 使用队列来实现
  • 空间复杂度为O(w),w是树的最大宽度
  • 适用场景:找最短路径、层次遍历、连通性检测

举个具体例子:

1
2
3
4
5
    1
/ \
2 3
/ \
4 5

DFS顺序:1 -> 2 -> 4 -> 5 -> 3
BFS顺序:1 -> 2 -> 3 -> 4 -> 5

在实际应用中,比如在社交网络中找朋友关系,如果要找最近的共同好友,用BFS比较合适;如果要遍历整个关系网络,DFS可能更节省内存。"

总结反思

  • 基本思想解释正确
  • 可以补充实现方式和复杂度分析
  • 应该结合具体例子来说明

问题9:代码题环节

面试官问题

“现在做个代码考核,时间大概15到20分钟。”

我的回答

[在代码环节遇到了协程相关的问题,最后没有完全解决]
“这个我实在是想不起来了…这个协程不怎么用。”

标准/建议回答

代码题部分确实是我的薄弱环节,主要暴露了以下问题:

"虽然我了解Go语言的基本语法和概念,但在实际编程实现上还需要更多练习。特别是协程相关的代码,平时虽然知道理论,但手写代码的经验不足。

这提醒我需要:

  1. 多做一些Go语言的编程练习,特别是并发相关的
  2. 熟悉常见的代码模式和最佳实践
  3. 准备一些经典的代码题,比如协程池、生产者消费者等
  4. 平时开发中应该更多地使用Go的并发特性

建议其他同学在准备面试时,不仅要掌握理论知识,还要有足够的编码实践,这样在面试中才能更好地展现自己的技术能力。"

总结反思

  • 理论知识掌握尚可,但编程实践不足
  • 需要加强代码题的训练
  • 应该准备一些常见的Go并发编程模式

面试整体感受

  • 难度评价: 适中,主要考察基础知识和理解深度
  • 面试官风格: 专业友善,会根据回答情况进行深入追问
  • 题目类型: 主要考察Go语言基础、数据库原理、数据结构算法,还有编程实践能力
  • 准备建议: 需要系统复习Go语言特性,加强编程实践,准备常见的技术问题

面试结果

  • 当场反馈: 面试官没有给出明确反馈,整体氛围比较友好
  • 后续流程: 等待HR通知,但最终没有进入下一轮
  • 个人感受: 基础知识回答还可以,但代码实现能力有待提高

经验总结

做得好的地方

  • 项目介绍比较清晰,技术栈匹配度较高
  • 基础概念掌握相对扎实
  • 面试过程中比较诚实,不会的地方坦诚承认

需要改进的地方

  • 部分概念理解不够深入,如垃圾回收机制
  • 编程实践能力需要加强,特别是并发编程
  • 应该准备更多的具体代码示例

准备建议

  1. 系统学习Go语言:不仅要知道是什么,还要知道为什么,以及如何应用
  2. 加强编程练习:多做一些Go语言的实际项目和代码题
  3. 深入理解原理:对于数据库、缓存等基础设施要有更深入的理解
  4. 准备项目细节:能够深入讲解项目中的技术决策和实现细节
  5. 关注业务场景:了解目标公司的业务,准备相关的技术问题

知识点复习

  • Go语言GMP模型和调度机制
  • Go语言内存管理和垃圾回收
  • Go并发编程最佳实践
  • MySQL索引原理和优化
  • Redis数据结构和应用场景
  • 常见数据结构和算法的实现
  • 微服务架构设计模式