腾讯不愧是大厂,面试太难了

wangzhongyang007 · · 115 次点击 · · 开始浏览    

今天分享一篇**腾讯的面经**,面经的主人公有**3年的Golang开发经验**,岗位的薪资为`25-30K`,内容我已经整理好了,看看难度如何: ### 1. Go的调度机制 > 主要就是回答GMP模型 #### G-P-M 模型 - **G (Goroutine)** :代表用户代码中的一个 Goroutine,是 Go 中最小的执行单元。 - **P (Processor)** :每个 P 表示一个逻辑处理器,负责管理一组可运行的 goroutine。默认情况下,P 的数量等于系统的 CPU 核心数,但可以通过 `runtime.GOMAXPROCS()` 函数调整。 - **M (Machine)** :对应于一个真实的操作系统线程,M 执行实际的代码。M 可以获取 P 来执行其上的 goroutine,当 M 阻塞时(例如进行系统调用),它会释放 P,让其他 M 获取并继续执行任务。 #### 工作窃取调度 - **本地队列**:每个 P 拥有一个本地的工作队列,用于存放待执行的 goroutine。这减少了锁争用,提高了性能。 - **全局队列**:除了本地队列外,还有一个全局队列,用于分配新的 goroutine 给各个 P。 - **工作窃取**:当一个 M 关联的 P 上的本地队列为空时,M 会尝试从其他 P 的本地队列中“窃取”一半的任务来执行。这种机制有助于平衡负载,尤其是在多核处理器上。 ### 2. Go的struct能否进行比较 > **可以**,Go中的struct可以进行比较。在Go语言中,结构体类型是可以比较的,**只有当结构体中的所有字段都是可以比较的类型时才可以进行比较**。如果结构体中的字段包含了**不可比较的类型**(比如切片、map等),则结构体就**不能**进行比较。 > > 在进行结构体比较时,会**逐个字段**进行比较,如果所有字段的值都相等,则认为两个结构体相等。需要注意的是,结构体比较是值比较,即比较的是结构体实例的**具体值**,而不是**引用或指针**。 ### 3. Go中的defer关键字使用 >defer关键字在Go中用于**延迟(defer)函数**的执行,即在函数执行完毕后再执行defer函数。defer函数通常用于资源释放、日志记录、错误处理等场景。defer语句会在函数返回之前执行,多个defer语句按照先进后出的顺序执行。 ### 4. select语句的用途 >select语句在Go语言中**主要用于实现并发控制和通信操作**。通过select语句,可以在多个通信操作中选择一个进行执行,而其他通信操作将被阻塞。在Go语言中,select语句通常与channel配合使用,用于在多个channel间进行数据传输和同步操作。 ### 5. context包的作用 #### 1. 超时和截止时间 `context` 包允许为操作设置超时或截止时间。这对于防止长时间运行的操作(如数据库查询或 HTTP 请求)阻塞整个应用程序非常有用。通过设置超时或截止时间,可以确保在指定时间内未完成的任务被自动取消,从而避免资源浪费。 - **WithTimeout**:用于创建一个带有固定超时的上下文。 - **WithDeadline**:用于创建一个带有绝对截止时间的上下文。 #### 2. 取消操作 `context` 提供了机制来显式地取消一个或多个 goroutine 的执行。这对于用户取消请求或者父 goroutine 完成工作后通知子 goroutine 停止工作非常有用。 - **WithCancel**:创建一个可以被显式取消的上下文。调用取消函数后,所有监听该上下文的 goroutine 都会收到取消信号并停止执行。 - **WithValue**:可以在上下文中传递键值对,这些值可以在同一请求链中的不同部分之间共享。虽然提供了这种功能,但应谨慎使用,以避免不必要的复杂性和性能开销。 #### 3. 传递请求范围的值 `context` 可以用来在同一个请求的不同部分之间传递请求范围的数据,例如用户的认证信息、请求ID等。这种方式有助于保持数据的一致性和可追踪性,同时减少了参数列表的长度。 ### 6. client如何实现长连接 > 要实现client端的长连接,**可以使用TCP协议**,通过保持连接不断开的方式来实现。 > >首先,在client端通过socket建立与server端的TCP连接,然后在连接建立后,client端和server端可以进行双向通信。为了实现长连接,client端需要保持连接不断开,可以**定时发送心跳包**给server端,以保持连接的活跃状态。 ### 7. 主协程如何等待其他协程完成后再操作 #### 使用 sync.WaitGroup `sync.WaitGroup` 是一个计数器,用于跟踪需要完成的任务数量。每个启动的 goroutine 应该调用 Add(1) 来增加计数器,当任务完成时调用 Done() 来减少计数器。主协程可以调用 Wait() 来阻塞,直到计数器归零,即所有任务都已完成。 **示例说明:** - 主协程创建一个 WaitGroup 实例,并为每个子协程调用 Add(1)。 - 每个子协程在完成任务后调用 Done()。 - 主协程调用 Wait(),等待所有子协程完成。 #### 使用通道 通道(`channel`)是 Go 语言中用于 goroutine 之间通信的机制。通过通道,主协程可以接收来自子协程的完成信号,从而知道何时继续执行。 **示例说明:** - 主协程创建一个通道,并启动多个子协程。 - 每个子协程在完成任务后向通道发送一个信号。 - 主协程通过 for range 或者固定次数的 for 循环接收这些信号,确保所有子协程都已完成。 ### 8. slice的扩容机制 - **1.7版本**:如果当前容量小于**1024**,则判断所需容量是否大于原来容量2倍,如果大于,当前容量加上所需容量;否则当前容量乘2。 - 如果当前容量大于1024,则每次按照1.25倍速度递增容量,也就是每次加上cap/4。 - **1.8版本**:Go1.18不再以1024为临界点,而是设定了一个值为**256**的`threshold`,以256为临界点;超过256,不再是每次扩容1/4,而是每次增加(旧容量+3*256)/4; - 当新切片需要的容量cap大于两倍扩容的容量,则直接按照新切片需要的容量扩容; - 当原 slice 容量 < threshold 的时候,新 slice 容量变成原来的 2 倍; - 当原 slice 容量 > threshold,进入一个循环,每次容量增加(旧容量+3*threshold)/4。 ### 9. map如何顺序读取 > 在Go中,**map是一种无序的数据结构**,因此不能保证按照特定顺序进行读取。如果需要按顺序读取map中的键值对,可以先**将键按照特定规则排序**,然后再按照排序后的键顺序读取对应的值。 ### 10. 如何实现一个set **用map模拟一个set**,把值置为struct{},struct{}本身不占任何空间,可以避免任何多余的内存分配。 ```Go type Set map[string]struct{} func main() { set := make(Set) for _, item := range []string{"A", "A", "B", "C"} { set[item] = struct{}{} } fmt.Println(len(set)) // 3 if _, ok := set["A"]; ok { fmt.Println("A exists") // A exists } } ``` ### 11. HTTP GET和HEAD请求的区别 >**`GET`请求会返回请求的资源,包括头部信息和实际数据,而`HEAD`请求只返回请求的资源的头部信息,不返回实际数据**。这样可以在不需要资源实际内容的情况下,只获取资源的元数据信息,比如文件大小、类型、修改时间等。在一些情况下,使用HEAD请求可以减少网络流量和加快响应速度。 ### 12. HTTP状态码401和403的区别 > HTTP状态码`401`代表**未授权**,表示客户端请求需要进行身份验证,而服务器拒绝了该请求,通常要求用户输入用户名和密码。 > > HTTP状态码`403`代表**禁止访问**,表示服务器理解了请求,但拒绝执行该请求,通常是因为服务器不允许访问特定资源或者没有权限执行该请求。 ### 13. HTTP keep-alive机制 >`HTTP keep-alive`机制是指**在一次TCP连接中可以传输多个HTTP请求和响应,而不是每次请求都要建立和关闭一个TCP连接**。这样可以减少TCP连接的建立和关闭次数,提高网络性能和资源利用率。 ### 14. HTTP是否可以在一次连接中发送多次请求而不等待后端返回 > HTTP是一种无状态协议,每个请求和响应之间是独立的,因此**在一次连接中发送多次请求是可能的,而且不需要等待后端返回**。这种技术被称为`HTTP pipelining`。在HTTP/1.1中是支持pipelining的,但并不是所有的服务器和客户端都支持这个特性。在实际应用中,由于某些服务器或代理可能不支持pipelining,因此可能会导致性能问题或错误的响应。 ### 15. TCP与UDP的区别,UDP的优点及适用场景 TCP与UDP是两种不同的传输层协议。`TCP是面向连接的,提供可靠的数据传输,而UDP是无连接的,提供不可靠的数据传输。` #### TCP和UDP的区别主要体现在以下几个方面: 1. **连接**:TCP是面向连接的,需要先建立连接,然后再进行数据传输,而UDP是无连接的,发送数据时无需建立连接。 2. **可靠性**:TCP提供可靠的数据传输,能够保证数据的完整性和顺序性,而UDP不提供可靠性,数据传输过程中可能会丢失或乱序。 3. **拥塞控制**:TCP具有拥塞控制机制,能够根据网络情况动态调整传输速率,而UDP不具备拥塞控制。 4. **首部开销**:TCP的首部开销较大,包含连接状态、序号、确认号等信息,而UDP的首部开销较小,只包含源端口、目的端口和长度等基本信息。 #### UDP的优点主要包括: 1. **低开销**:UDP的首部开销小,传输效率高。 2. **实时性**:UDP不需要建立连接,传输速度快,适用于对实时性要求较高的应用场景。 3. **简单性**:UDP相对于TCP更简单,实现和维护成本低。 #### UDP适用场景包括: 1. **实时音视频传输**:如在线视频会议、直播等,对实时性要求高。 2. **DNS查询**:域名解析过程中的数据传输,要求快速响应。 3. **广播或多播应用**:如在线游戏中的数据广播等。 ### 16. time-wait状态的作用 > `time-wait`状态是指**TCP连接关闭后,等待一段时间才能完全释放资源的状态**。在这段时间内,系统会保持连接的信息,以便在网络中的数据传输完整,同时避免出现数据混乱或重复的情况。 ### 17. 孤儿进程和僵尸进程的区别 >**孤儿进程是指父进程先于子进程结束,而子进程成为孤儿进程**,此时孤儿进程会被 init 进程(PID为1)接管,并由 init 进程负责回收孤儿进程的资源,保证不会成为僵尸进程。 > >**僵尸进程是指子进程先于父进程结束,而父进程没有及时回收子进程的 PCB(Process Control Block),导致子进程的进程描述符仍然存在**,但已经无法运行,此时子进程就会成为僵尸进程。 ### 18. 死锁的条件及如何避免 死锁(Deadlock)是指两个或多个进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。为了发生死锁,必须同时满足四个必要条件,这些条件被称为 **Coffman 条件** 或 **死锁定理**,四个必要条件是:**互斥条件、占有并等待条件、 不可抢占条件和循环等待条件**。 #### 如何避免死锁 为了避免死锁,可以采取不同的策略来破坏上述四个条件中的一个或多个。以下是几种常见的避免死锁的方法: ##### 1. 破坏“占有并等待”条件 - **一次性分配所有资源**:要求每个进程在开始执行前申请它需要的所有资源。如果不能立即获得所有资源,则该进程必须等待,直到所有资源都可用为止。这种方法虽然简单,但可能导致资源利用率低和饥饿问题。 - **按序分配资源**:为资源编号,规定进程必须按照某种顺序(如递增或递减)申请资源。这样可以防止循环等待的发生。 ##### 2. 破坏“不可抢占”条件 - **允许抢占资源**:如果一个进程持有的资源不能满足其需求,它可以被强制释放资源,然后重新申请所需的所有资源。这需要设计复杂的资源管理和恢复机制,以确保数据的一致性和完整性。 ##### 3. 破坏“循环等待”条件 - **资源排序法**:给所有的资源分配一个全局唯一的编号,要求进程只能按照编号递增的顺序申请资源。这样可以有效地避免循环等待,因为任何一个进程都不会等待比自己当前持有资源编号小的资源。 - **银行家算法**:这是一个更复杂的算法,用于检测系统是否处于安全状态。它通过模拟资源分配来预测未来的资源需求,从而决定是否允许进程继续运行。如果分配后系统仍然处于安全状态,则允许分配;否则拒绝分配并让进程等待。 ##### 4. 检测与恢复 - **死锁检测**:定期检查系统中是否存在死锁。如果发现死锁,可以选择一种或多种方法来解除死锁,例如回滚某些进程、杀死某些进程或者重启整个系统。这种方法不需要事先预防死锁,但需要额外的开销来进行检测和恢复。 - **超时机制**:为每个操作设置一个合理的超时时间。如果一个进程在指定时间内没有完成任务,则认为可能发生了死锁,并采取相应的措施。 ### 19. 常用的Linux命令:查看端口占用、CPU负载、内存占用,如何发送信号给一个进程 - **常用的Linux命令**包括:ls(列出目录内容)、cd(切换目录)、pwd(显示当前目录)、cp(复制文件或目录)、mv(移动文件或目录)、rm(删除文件或目录)、mkdir(创建目录)、rmdir(删除空目录)、top(查看系统资源占用情况)、ps(显示当前进程信息)、kill(终止进程)、ifconfig(查看网络接口信息)、netstat(查看网络连接信息)、grep(搜索文本)、tar(打包与解压)、chmod(修改文件权限)等。 - **要查看端口占用情况,可以使用netstat命令或者lsof命令**。netstat -tuln可以查看当前所有TCP和UDP端口的占用情况,而lsof -i:端口号可以查看指定端口的占用情况。 - **要查看CPU负载,可以使用top命令或者uptime命令**。top命令可以实时查看系统资源的占用情况,包括CPU、内存等,而uptime命令可以显示系统的平均负载。 - **要查看内存占用情况,可以使用free命令或者top命令**。free命令可以显示系统内存的使用情况,包括已使用、空闲等信息,而top命令也可以显示内存占用情况。 - **要发送信号给一个进程,可以使用kill命令**。首先使用ps命令找到要发送信号的进程的PID,然后使用kill -信号 PID来发送信号。常用的信号包括SIGTERM(15,终止进程)、SIGKILL(9,强制终止进程)、SIGHUP(1,重启进程)等。 ### 20. Git的文件版本管理,merge和rebase的区别 >在Git中,**merge和rebase都是用来整合不同分支的修改内容的方法**。merge会将两个分支的修改内容合并到一起,形成一个新的提交,而rebase会将当前分支的修改“挪动”到目标分支的最新提交之后。 ## 欢迎关注 ❤ 我们搞了一个**免费的面试真题共享群**,互通有无,一起刷题进步。 **没准能让你能刷到自己意向公司的最新面试题呢。** 感兴趣的朋友们可以加我微信:**wangzhongyang1993**,备注:面试群。

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

115 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传