面试基本信息
- 公司: 蔚来汽车(新能源汽车独角兽)
- 职位: 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 | func processFile() error { |
总结反思:
- 当时说成了"先进先出",应该是"后进先出"(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 | if redis.call("get", KEYS[1]) == ARGV[1] then |
当然,Redis分布式锁也有一些局限性,比如在主从架构下可能出现锁丢失的问题,这时候可以考虑使用Redlock算法或者其他分布式锁方案。"
总结反思:
- 当时确实没有实际使用经验,回答很简单
- 应该至少了解基本的实现原理
- 可以补充学习一些实际的使用场景
问题7:队列和栈的区别及应用场景
面试官问题:
“说一下队列和栈有什么区别,他们分别可以在业务上举个例子,在什么样的场景下去使用。”
我的回答:
"队列的话一般是用在这种先进先出的情况。就比如说我刚刚说的那个消息队列,它有一种也是一种队列的数据结构。它这样的话就可以保证先生成的订单被,就是先生成的订单会先处理。
然后栈的话是这样一种先进后出的一种数据结构。然后它一般来说的话就是比如说我们在实行一个递归的操作的时候,它的那个函数其实就是一个用栈的实现来,用这样的方式来实现的。"
标准/建议回答:
队列和栈的区别及应用,我觉得当时回答得还可以,可以再丰富一些:
"队列(Queue)是先进先出(FIFO)的数据结构,栈(Stack)是后进先出(LIFO)的数据结构。
队列的典型应用场景:
- 消息队列:如你提到的订单处理,保证请求按顺序处理
- 广度优先搜索(BFS):层次遍历树或图
- 任务调度:操作系统的进程调度,打印机任务队列
- 缓冲区:生产者消费者模型中的数据缓冲
栈的典型应用场景:
- 函数调用栈:存储函数调用的上下文信息
- 深度优先搜索(DFS):递归遍历的模拟
- 表达式求值:中缀表达式转后缀表达式
- 浏览器历史记录:后访问的页面先返回
- 撤销操作:编辑器的Ctrl+Z功能
在实际开发中,比如做一个计算器应用,解析数学表达式时就需要用栈来处理括号匹配和运算符优先级。而做一个任务系统时,通常用队列来保证任务的公平调度。"
总结反思:
- 基本概念回答正确
- 应用场景可以更丰富一些
- 可以结合具体的技术实现来说明
问题8:深度优先遍历和广度优先遍历
面试官问题:
“说一下深度优先遍历和广度优先遍历分别是什么样的一种遍历方式。”
我的回答:
"深度优先遍历的话,我直接在树这个结构,就不在图里面去讲了。比如说在树这个结构下的话,深度优先遍历的话,它会从根节点开始一级级遍历。它每次只会选择一个分支去进行遍历,然后最后碰到了叶子节点以后再返回,不一定是叶子节点,就是碰到了比如说它是先向左遍历,那它碰到没有左节点以后,它会继续向右遍历。然后相当于它就先往深处遍历,然后慢慢再回溯,然后再往深处遍历,是这样一个操作过程。
而广度优先遍历的话它是一层一层的遍历,它就是有种那种比较发散的那种感觉。"
标准/建议回答:
关于DFS和BFS,我觉得可以这样解释会更清楚:
"深度优先遍历(DFS)和广度优先遍历(BFS)是两种不同的图/树遍历策略:
深度优先遍历的特点:
- 尽可能深地访问节点,直到无法继续为止,然后回溯
- 使用栈(递归或显式栈)来实现
- 空间复杂度相对较低,为O(h),h是树的高度
- 适用场景:找路径、检测环、拓扑排序等
广度优先遍历的特点:
- 逐层访问节点,先访问距离根节点近的节点
- 使用队列来实现
- 空间复杂度为O(w),w是树的最大宽度
- 适用场景:找最短路径、层次遍历、连通性检测
举个具体例子:
1 | 1 |
DFS顺序:1 -> 2 -> 4 -> 5 -> 3
BFS顺序:1 -> 2 -> 3 -> 4 -> 5
在实际应用中,比如在社交网络中找朋友关系,如果要找最近的共同好友,用BFS比较合适;如果要遍历整个关系网络,DFS可能更节省内存。"
总结反思:
- 基本思想解释正确
- 可以补充实现方式和复杂度分析
- 应该结合具体例子来说明
问题9:代码题环节
面试官问题:
“现在做个代码考核,时间大概15到20分钟。”
我的回答:
[在代码环节遇到了协程相关的问题,最后没有完全解决]
“这个我实在是想不起来了…这个协程不怎么用。”
标准/建议回答:
代码题部分确实是我的薄弱环节,主要暴露了以下问题:
"虽然我了解Go语言的基本语法和概念,但在实际编程实现上还需要更多练习。特别是协程相关的代码,平时虽然知道理论,但手写代码的经验不足。
这提醒我需要:
- 多做一些Go语言的编程练习,特别是并发相关的
- 熟悉常见的代码模式和最佳实践
- 准备一些经典的代码题,比如协程池、生产者消费者等
- 平时开发中应该更多地使用Go的并发特性
建议其他同学在准备面试时,不仅要掌握理论知识,还要有足够的编码实践,这样在面试中才能更好地展现自己的技术能力。"
总结反思:
- 理论知识掌握尚可,但编程实践不足
- 需要加强代码题的训练
- 应该准备一些常见的Go并发编程模式
面试整体感受
- 难度评价: 适中,主要考察基础知识和理解深度
- 面试官风格: 专业友善,会根据回答情况进行深入追问
- 题目类型: 主要考察Go语言基础、数据库原理、数据结构算法,还有编程实践能力
- 准备建议: 需要系统复习Go语言特性,加强编程实践,准备常见的技术问题
面试结果
- 当场反馈: 面试官没有给出明确反馈,整体氛围比较友好
- 后续流程: 等待HR通知,但最终没有进入下一轮
- 个人感受: 基础知识回答还可以,但代码实现能力有待提高
经验总结
做得好的地方
- 项目介绍比较清晰,技术栈匹配度较高
- 基础概念掌握相对扎实
- 面试过程中比较诚实,不会的地方坦诚承认
需要改进的地方
- 部分概念理解不够深入,如垃圾回收机制
- 编程实践能力需要加强,特别是并发编程
- 应该准备更多的具体代码示例
准备建议
- 系统学习Go语言:不仅要知道是什么,还要知道为什么,以及如何应用
- 加强编程练习:多做一些Go语言的实际项目和代码题
- 深入理解原理:对于数据库、缓存等基础设施要有更深入的理解
- 准备项目细节:能够深入讲解项目中的技术决策和实现细节
- 关注业务场景:了解目标公司的业务,准备相关的技术问题
知识点复习
- Go语言GMP模型和调度机制
- Go语言内存管理和垃圾回收
- Go并发编程最佳实践
- MySQL索引原理和优化
- Redis数据结构和应用场景
- 常见数据结构和算法的实现
- 微服务架构设计模式