写点什么

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

作者:王中阳Go
  • 2024-12-25
    湖南
  • 本文字数:5306 字

    阅读完需:约 17 分钟

今天分享一篇腾讯的面经,面经的主人公有 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{}本身不占任何空间,可以避免任何多余的内存分配。


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,备注:infoq 面试群。

用户头像

王中阳Go

关注

靠敲代码在北京买房的程序员 2022-10-09 加入

【微信】wangzhongyang1993【公众号】程序员升职加薪之旅【成就】InfoQ专家博主👍掘金签约作者👍B站&掘金&CSDN&思否等全平台账号:王中阳Go

评论

发布
暂无评论
腾讯不愧是大厂,面试太难了_Go_王中阳Go_InfoQ写作社区