今天要和大家分享的是我们[训练营内部](https://mp.weixin.qq.com/s/tKbBrdwOzLCvQv_FqkVvQg?token=1789994108&lang=zh_CN)整理的**腾讯校招**的一面面经。这次面试特别聚焦于**Go语言**的知识点,几乎是把能问到的都问了个遍,而且问题相当**细致**。我已经把所有的问题和答案都整理好了,希望对大家有帮助:
![](https://files.mdnice.com/user/76962/162a8c20-90b4-465e-8981-228fd05c11ca.png)
## 面试答案
### 1. map怎么去做并发安全
- 可以使用互斥锁(sync.Mutex)。在访问map之前加锁,访问完成之后解锁,保证同一时间只有一个协程能够访问map。例如可以定义一个包含map和互斥锁的结构体,在对map进行读写操作的方法中先获取锁,操作完成后释放锁。
- 使用读写锁(sync.RWMutex)。如果对map的读操作远多于写操作,可以使用读写锁。多个协程可以同时读取map(加读锁),但写操作(加写锁)是互斥的,这样能提高并发性能。
- 使用Go 1.9引入的sync.Map。它是专门为并发场景设计的map类型,内部有复杂机制保证在多个协程读写时的安全性,其操作方法(如Store、Load、Delete等)可以在多个协程中安全地调用,不需要额外的锁操作。
### 2. 外层的协程能捕获子协程的panic吗
- 在Go语言中,协程是相互独立的执行单元。当一个子协程发生`panic`时,它会在自己的执行栈中进行异常处理流程。通常情况下,外层协程无法直接捕获子协程的`panic`。
- 这是因为`panic`和`recover`的机制是基于当前协程的执行栈。`recover`函数只能在`defer`语句中使用,并且只能恢复当前协程中发生的`panic`。例如,主协程中的`recover`函数不能捕获子协程中抛出的`panic`。
- 从执行流程角度看,每个协程就像是一个独立的小世界,当子协程出现`panic`,它会在自己的世界里按照`panic`的处理规则(如逆序执行`defer`函数)进行处理,这个过程不会自动和外层协程交互,使得外层协程不能简单地捕获子协程的`panic`。
### 3. panic都会被捕获吗?哪些panic不会捕获
- 不是所有的panic都会被捕获。如果没有在合适的位置使用`defer`和`recover`来处理panic,那么当panic发生时,程序会沿着函数调用栈向上回溯,直到找到可以处理它的`recover`调用或者程序直接崩溃。例如,在一个没有任何`defer - recover`机制的简单函数中发生了panic,并且这个函数没有被其他可以捕获panic的函数调用,那么这个panic就不会被捕获,程序会直接退出。
- 而在有`defer`和`recover`的函数或者协程中,当发生panic时,`recover`可以捕获到panic的值,并且可以在这个函数内部进行处理,阻止panic继续向上传播导致程序崩溃。
### 4. slice和数组的区别?底层结构
- **区别**:
- 数组的长度是固定的,在定义数组时就必须指定其长度,并且这个长度不能改变。而slice的长度是可变的,可以通过`append`等操作来动态地增加或减少其长度。
- 数组是值类型,当把一个数组赋值给另一个变量或者作为参数传递给函数时,会进行值的复制,可能会占用较多的内存。而slice是引用类型,它包含一个指向底层数组的指针、长度和容量。将一个slice赋值给另一个变量或者传递给函数时,只是复制了这三个属性,不会复制底层数组,因此更加高效。
- **底层结构**:
- 数组在内存中是一块连续的存储空间,存储了一组相同类型的数据元素。
- slice的底层结构包含三个部分,一个指针,指向底层数组的起始位置;一个长度,表示当前slice中元素的个数;一个容量,表示底层数组从指针位置开始到末尾的元素个数。
### 5. go哪些内置类型是并发安全的
- 原子类型(如`sync/atomic`包中的类型)是并发安全的,例如`atomic.Value`可以在多个协程中安全地存储和读取任意类型的值。
- `sync.Mutex`和`sync.RWMutex`本身也是并发安全的,用于实现互斥和读写锁的功能。
- `sync.Once`用于保证某个操作只执行一次,是并发安全的。
- `sync.WaitGroup`用于协程的同步,在多个协程中正确使用时是并发安全的,它可以用来等待一组协程完成。
- `sync.Cond`用于条件变量,是并发安全的,可用于协程之间的同步等待某个条件满足。
- `sync.Map`是一个并发安全的map类型。
### 6. go的结构体可以嵌套组合吗
- Go的结构体可以嵌套组合。可以在一个结构体定义中包含另一个结构体作为其字段。例如,定义一个`Person`结构体包含`Name`和`Address`两个字段,其中`Address`可以是另一个结构体,包含`City`、`Street`等字段。
- 这种嵌套组合的方式可以方便地构建复杂的数据结构,并且可以通过点操作符来访问嵌套结构体中的字段,例如`person.Address.City`。
### 7. 两个结构体可以等值比较吗
- 如果结构体的所有字段都是可以比较的(如基本类型、指针类型等),那么两个结构体可以进行等值比较。当进行比较时,会按照字段的顺序逐个比较结构体中的字段。例如,有一个包含两个`int`字段的结构体`struct {a, b int}`,可以直接使用`==`运算符来比较两个这样的结构体是否相等,它会先比较第一个`int`字段,如果相等再比较第二个`int`字段。
- 但是,如果结构体中包含不可比较的字段(如`map`、`slice`类型等),那么这个结构体就不能直接使用`==`运算符进行比较。
### 8. 如何理解interface类型
- interface是一种抽象类型,它定义了一组方法签名。一个类型如果实现了interface中定义的所有方法,那么这个类型就实现了这个interface。例如,定义一个`Animal` interface,其中包含`Speak()`方法,那么任何结构体只要实现了`Speak()`方法,就可以被看作是实现了`Animal` interface。
- interface在Go语言中有很多用途,比如可以用于实现多态,使得代码更加灵活和可扩展。可以通过接口类型的变量来调用实现了该接口的具体类型的方法,而不需要关心具体的类型是什么。
### 9. 1.18版本后interface有什么增强
- Go 1.18版本对interface进行了泛型支持的增强。
- 这使得interface可以与泛型结合使用,更加灵活地定义和使用抽象类型。例如,可以定义带有类型参数的interface,这些类型参数可以在具体实现中被替换为具体的类型,从而可以更好地处理不同类型的数据,并且在编译时可以进行更严格的类型检查,提高代码的安全性和可维护性。
### 10. interface可以进行等值比较吗
- interface可以进行等值比较。如果两个interface变量的动态类型相同且动态值相等,那么它们相等。例如,如果有两个interface变量,一个是实现了某个接口的结构体A的实例,另一个也是结构体A的实例,并且它们的字段值都相等,那么这两个interface变量相等。
- 但是如果两个interface变量的动态类型不同,即使它们的底层值在某种程度上看起来相似,它们也不相等。
### 11. 说说逃逸分析
- 逃逸分析是指编译器在编译时会分析变量的作用域和生命周期,判断变量是否会“逃逸”出它的定义函数。
- 如果一个变量在函数内部定义,但是其引用被传递到函数外部(例如作为函数返回值或者存储在一个全局变量中),那么这个变量就发生了逃逸。编译器会根据逃逸分析的结果来决定变量是在栈上分配内存还是在堆上分配内存。如果变量没有逃逸,通常会在栈上分配内存,这样在函数返回时,变量占用的内存会自动被回收,效率较高。如果变量发生了逃逸,就会在堆上分配内存,并且需要通过垃圾回收(GC)来回收内存。
### 12. channel有缓冲和无缓冲的区别
- **无缓冲channel**:也称为同步channel。当一个协程向无缓冲channel发送数据时,这个发送操作会阻塞,直到另一个协程从这个channel接收数据。同样,当一个协程从无缓冲channel接收数据时,这个接收操作会阻塞,直到另一个协程向这个channel发送数据。它主要用于协程之间的同步通信,保证数据的发送和接收是同时进行的。
- **有缓冲channel**:当一个协程向有缓冲channel发送数据时,如果缓冲区没有满,发送操作不会阻塞,可以继续发送。当一个协程从有缓冲channel接收数据时,如果缓冲区中有数据,接收操作不会阻塞,可以直接接收。只有当缓冲区满(发送操作)或者空(接收操作)时,才会发生阻塞。它可以在一定程度上解耦协程之间的通信,允许一定程度的异步操作。
### 13. map并发访问会怎么样?这个异常可以捕获吗?
- 当多个协程并发访问(读写)一个普通的map时,可能会出现竞态条件,导致程序出现不可预期的行为,比如程序崩溃或者数据不一致。例如,一个协程在读取一个map中的元素,同时另一个协程在删除这个元素,可能会导致读取到错误的数据或者程序出现`panic`。
- 这种由于并发访问map导致的`panic`是可以捕获的,但是即使捕获了`panic`,也很难正确地处理这种并发冲突导致的错误数据等问题。更好的做法是使用并发安全的`sync.Map`来避免这种情况。
### 14. GMP模型
- GMP模型是Go语言的运行时调度模型,其中G代表Goroutine(协程),M代表Machine(系统线程),P代表Processor(逻辑处理器)。
- 协程(G)是Go语言中轻量级的用户级线程,它是实际执行代码的单元。系统线程(M)是操作系统层面的线程,用于执行协程。逻辑处理器(P)是Go运行时对线程的抽象,它可以理解为一个资源容器,每个P都有一个本地队列,用于存放待执行的G。当创建一个协程时,它会被放入某个P的本地队列或者全局队列中,然后由M从队列中取出G来执行。P的数量可以通过`GOMAXPROCS`环境变量或者函数来设置,它决定了同时可以执行的协程数量的上限(在一定程度上)。
### 15. GMP模型中什么时候把G放全局队列?
- 当本地队列已满,新创建的协程(G)会被放入全局队列。
- 另外,当某个P从它的本地队列中取出协程(G)执行时,如果本地队列已经空了,它会尝试从全局队列中获取协程来执行。这样可以保证协程在不同的逻辑处理器(P)之间能够比较均衡地分配,避免某些P空闲而其他P负载过重的情况。
### 16. go的gc
- Go语言的垃圾回收(GC)是自动管理内存的机制。它会自动检测不再使用的内存并进行回收,这样程序员不需要手动释放内存,减少了内存管理的负担和出错的可能性。
- Go的GC采用了标记 - 清除(mark - sweep)算法的变体,例如三色标记法。在标记阶段,会从根对象开始遍历所有可达的对象,标记为存活状态。在清除阶段,会回收未被标记的对象占用的内存。Go的GC还在不断地优化,例如减少GC的停顿时间,提高程序的性能。
### 17. gc扫描是并发的吗?
- Go的GC扫描在一定程度上是并发的。
- Go语言的垃圾回收器会尽量减少对程序执行的阻塞,采用了一些并发的策略。例如,在标记阶段可以和程序的执行并发进行,通过一些手段(如写屏障等)来保证标记的准确性,这样可以减少GC导致的程序停顿时间,提高程序的运行效率。
### 18. gc中的根对象是什么?
- 在Go语言的垃圾回收(GC)中,根对象是指那些可以直接访问到的对象,它们是GC标记过程的起点。
- 根对象包括全局变量、栈上的变量(因为栈上的变量可以被当前执行的函数访问)、寄存器中的对象等。从这些根对象开始,垃圾回收器会通过指针遍历所有可达的对象,标记为存活状态,而不可达的对象则会在清除阶段被回收。
### 19. 项目中etcd用来干什么的?
- etcd是一个分布式的键值存储系统,主要用于分布式系统中的配置管理、服务发现和分布式锁等功能。
- 在配置管理方面,它可以存储系统的各种配置信息,多个服务可以从etcd中获取配置,并且当配置发生变化时,etcd可以通知相关的服务。在服务发现中,服务可以将自己的信息(如IP地址、端口等)注册到etcd中,其他服务可以通过查询etcd来发现可用的服务。对于分布式锁,etcd可以通过其原子操作等特性来实现分布式环境下的互斥锁,保证在多个节点访问共享资源时的一致性。
### 20. mysql索引B+T
- B+树(B+ Tree)是MySQL中索引的一种常见数据结构。它是B树的一种变体。
- B+树的特点是所有的数据都存储在叶子节点,非叶子节点只存储索引信息。叶子节点之间通过指针相互连接,形成一个有序链表。在查询数据时,从根节点开始,通过比较索引值,逐步向下查找,最终在叶子节点找到所需的数据。这种结构使得B+树在范围查询(如查询某个区间内的所有数据)和排序查询方面表现优异,因为可以通过叶子节点的链表快速地遍历相关的数据。同时,它的高度相对较低,减少了查询时磁盘I/O的次数,提高了查询效率。
### 21. 索引的优缺点
- **优点**:
- 可以大大提高查询速度。例如,在一个包含大量数据的表中,通过索引可以快速定位到符合条件的数据,减少了全表扫描的时间。
- 对于排序和分组操作,合适的索引可以提高效率。因为索引本身是一种有序的数据结构,所以在进行排序或分组时,可以利用索引的顺序性,减少额外的排序操作。
- **缺点**:
- 增加了数据库的存储空间。索引本身需要占用一定的磁盘空间,尤其是在表中有大量数据和多个索引的情况下,索引占用的空间可能会比较可观。
- 降低了数据更新(插入、删除、修改)的速度。因为每次更新数据时,可能需要同时更新索引,这会增加额外的操作时间,尤其是在索引结构比较复杂的情况下。
### 22. redis用来做什么的?
- Redis是一个高性能的键值存储数据库,主要用于缓存、消息队列、计数器、排行榜等多种应用场景。
- 在缓存方面,它可以存储经常访问的数据,减少对后端数据库(如MySQL)的访问压力,提高系统的响应速度。例如,在一个Web应用中,将页面的部分数据存储在Redis中,当用户再次请求相同页面时,可以直接从Redis中获取数据,而不需要重新查询数据库。对于消息队列,Redis可以通过其列表(List)等数据结构来实现简单的消息队列功能,用于异步处理任务。作为计数器,Redis可以方便地对某个事件的发生次数进行计数,比如网站的访问次数。在排行榜应用中,Redis可以利用有序集合(Sorted Set)来实现排行榜功能,比如游戏中的玩家分数排行榜。
### 23. redis过期淘汰策略
- 定时删除:在设置键的过期时间时,同时创建一个定时器,当键过期时,立即删除该键。这种策略可以及时释放内存,但是会占用大量的CPU资源来处理定时器。
- 惰性删除:当访问一个键时,先检查它是否过期,如果过期则删除。这种策略节省了CPU资源,但是会导致过期键可能长时间占用内存,直到被访问。
- 定期删除:每隔一段时间,对数据库进行一次检查,删除过期的键。这种策略是定时删除和惰性删除的一种折中,通过定期扫描来清理过期键,减少过期键占用内存的时间,同时也不会像定时删除那样占用过多的CPU资源。Redis实际使用的是惰性删除和定期删除相结合的策略,以平衡CPU资源和内存占用的关系。
## 欢迎关注 ❤
我们搞了一个**免费的面试真题共享群**,互通有无,一起刷题进步。
**没准能让你能刷到自己意向公司的最新面试题呢。**
感兴趣的朋友们可以加我微信:**wangzhongyang1993**,备注:面试群。
有疑问加站长微信联系(非本文作者))