大家好,我是皇子。
上周阿里巴巴正式启动春招,一位校招同学反馈面得不太好,主要是自己准备不充分,基础不够扎实,知识面不够广。
既然错过了阿里,就冲腾讯吧,昨天腾讯刚启动了 2024 年实习生招聘计划,可以说技术类岗位与阿里一样占比最高,阿里 70%,腾讯 60%,较之前两年有显著增长。

腾讯校招的流程为:投递简历-笔试(120 分钟)-面试-集体面试-初试-复试-HR面试-录用评估-录取意向确认-签约。
那么腾讯的技术笔试面试难度到底如何呢?分享一位校招程序员同学,腾讯的 Golang 后端开发的面经,首先明确一点是笔试部分不设分数线,不存在通过或不通过的说法,笔试成绩仅供面试官参考;另外腾讯很多都转 Golang 了,Java 很少的。
好了,开始这次 Golang 后端开发的面经,考察的知识点,针对八股文涉及的内容,我帮大家列了一下:
包 (pkg) 是 Go 工作区中包含 Go 源文件或其他包的目录。源文件中的每个函数、变量和类型都存储在链接包中。每个 Go 源文件都属于一个包,该包在文件顶部使用以下命令声明:
package <packagename>
可以使用以下方法导入包以重用导出的函数或类型:
import <packagename>
Golang 的标准包有 fmt、log、errors等,其中包含格式化和打印功能,如 Println()
与其他作为学术实验开始的语言不同,Go 代码的设计是务实的。每个功能和语法决策都旨在让程序员的编码更轻松。
Golang 针对并发进行了优化,并且在规模上运行良好。
由于单一的标准代码格式,Golang 通常被认为比其他语言更具可读性。
自动垃圾收集明显比 Java 或 Python 更有效,因为它与程序同时执行。
make 的作用是为 slice, map or chan 的初始化 然后返回引用。make 函数是内建函数,函数定义:
func make(Type, size IntegerType) Type
make(T, args)函数的目的和 new(T)不同,make仅仅用于创建 slice, map, channel 而且返回类型是实例。
切片是基于数组实现的,本身并不是动态数组或者数组指针
切片内部实现的数据结构(指向底层数组的指针、切片的长度、切片的容量)通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内
切片本身本身非常小不拥有任何数据,只是对现有数组的引用,可以理解为对底层数组的抽象
因为基于数组实现,所以它的底层的内存是连续分配的,效率非常高,还可以通过索引获得数据。
Go 1.18 之前
Go 中切片扩容的策略是这样的:
首先判断,如果新申请容量大于 2 倍的旧容量,最终容量就是新申请的容量
否则判断,如果旧切片的长度小于 1024,则最终容量就是旧容量的两倍
否则判断,如果旧切片长度大于等于 1024,则最终容量从旧容量开始循环增加原来的 1/4, 直到最终容量大于等于新申请的容量
如果最终容量计算值溢出,则最终容量就是新申请容量
Go 1.18
内置函数 Append 对切片的扩容算法发生变化:和 Go 1.17 以1024作为大小分界不同,Go 1.18 使用 256 作为 threshold。
l 情况一:
如果 slice 还有足够的容量,添加新元素时不会发生扩容,因此扩容前后的 slice 底层数组是相同的。在这种情况下,添加新元素只会改变 slice 的长度,而不会改变容量。
需要注意的是,如果 slice 的容量不足以容纳新元素,添加新元素时就会发生扩容,此时扩容前后的 slice 底层数组是不同的。因此,在使用 slice 时,我们应该尽可能地预估其容量,以避免不必要的扩容操作,提高程序的性能。
l 情况二:
在 Go 语言中,当一个 slice 的容量不足以容纳更多的元素时,它就会自动扩容。这个过程中,Go 会创建一个新的底层数组,并将原有的元素复制到新的数组中。因此,扩容前后的 slice 底层数组是不同的。
具体来说,当一个 slice 的长度达到其容量时再向其添加元素时,Go 会按照以下步骤进行扩容:
扩容时创建一个新的底层数组
将原有数组中的元素复制到新的数组中。
将新的元素添加到新的数组中。
返回一个新的 slice,其底层数组为新创建的数组。
因此,扩容前后的 slice 底层数组是不同的,但是它们包含的元素是相同的。如果您需要比较两个 slice 是否相同,应该使用 Go 语言提的内置函数 reflect.DeepEqual() 进行比较。这个函数可以比较两个 slice 是否包含相同的元素,但是它不能比较两个 slice 的底层数组是否相同。
1.什么是并发?
并发:两个或两个以上的任务在一段时间内被执行。我们并不关心这些任务是否在同一时刻执行,我们只是知道,这些任务在这一段时间能能够都被执行,当然这一段时间可以很长,也可以很短。
2.并发的最小并发单位是什么?
进程是计算机资源分配最小的单位,是CPU分配资源的基本单位,具有独立的内存。
线程是计算机调度最小的单位,也是程序执行的最小单位,是在进程中的,一个进程往往会有一个到多个线程。
3.计算机是如何实现并发的?
计算机的分时调用是并发的根本,CPU通过快速的切换作业来执行不同的作业,基本的调度单位在执行的时候可以被阻塞掉,此时就会将CPU资源让出来,等到该调度单位再次被唤醒的时候,又可以使用CPU资源,而操作系统保证了整个的调度过程。
Go 语言作为一个原生支持用户态进程(Goroutine)的语言,当提到并发编程、多线程编程时,往往都离不开锁这一概念。锁是一种并发编程中的同步原语(Synchronization Primitives),它能保证多个 Goroutine 在访问同一片内存时不会出现竞争条件(Race condition)等问题。
Go 语言在 sync 包中提供了用于同步的一些基本原语,包括常见的 sync.Mutex、sync.RWMutex、sync.WaitGroup、sync.Once 和 sync.Cond。这些基本原语提供了较为基础的同步功能,但是它们是一种相对原始的同步机制,在多数情况下,我们都应该使用抽象层级更高的 Channel 实现同步。
一个 Goroutine(协程)获得 Mutex 锁:任何其他Goroutine(协程)只能等待该 Goroutine 释放该 Mutex 锁,才能继续执行。
一个 Goroutine(协程)获得了RWMutex 锁:在读锁占用的情况下,会阻止写但不阻止读 RWMutex;在写锁占用情况下,会阻止任何其他 Goroutine(无论读和写)进来,整个锁相当于由该 Goroutine 独占。
同步锁的作用是保证资源在使用时的独有性,不会因为并发而导致数据错乱, 保证系统的稳定性。
Goroutine是Golang 在语言级别支持的一种轻量级线程,称之为 Goroutine,。Golang 标准库提供的所有系统调用操作(包括所有的同步 I/O 操作),都会出让 CPU 给其他Goroutine。这让 Goroutine 的切换管理不依赖于系统的线程和进程,也不依赖于 CPU 的核心数量,而是交给 Golang 的运行时统一调度。
Go1.19
Go运行时将根据goroutine的历史平均栈使用率来分配初始goroutine栈,避免了一些goroutine的最多2倍的goroutine栈空间浪费。
G(Goroutine):我们所说的协程,为用户级的轻量级线程,每个 Goroutine 对象中的 sched 保存着其上下文信息。
M(Machine):对内核级线程的封装,数量对应真实的 CPU 数(真正干活的对象)。
P(Processor):即为 G 和 M 的调度对象,用来调度 G 和 M 之间的关联关系,其数量可通过 GOMAXPROCS()来设置,默认为核心数。
能照着图回答,面试官看你的眼神都会变
在1.2版本之前,go的调度器仍然不支持抢占式调度,程序只能依靠Goroutine主动让出CPU资源才能触发调度,这会引发一些问题,比如:
某些 Goroutine 可以长时间占用线程,造成其它 Goroutine 的饥饿
垃圾回收器是需要stop the world的。如果垃圾回收器想要运行了,那么它必须先通知其它的goroutine合作停下来,这会造成较长时间的等待时间。
1.13版本 go依赖调用函数栈增长检测代码的方式,即基于协作的抢占式调度(并非真正意义上的抢占式调度)
工作机制
编译器会在调用函数前插入 runtime.morestack,可能会调用runtime.newstack进行抢占
Go 语言运行时会在垃圾回收暂停程序、系统监控发现 Goroutine 运行超过 10ms 时,则发出抢占请求 StackPreempt
当发生函数调用时,可能会执行编译器插入的 runtime.morestack,它调用的 runtime.newstack会检查 Goroutine 的 stackguard0 字段是否为 StackPreempt;
如果 stackguard0 是 StackPreempt,就会触发抢占让出当前线程
但是这种调度并不完备,比如一个goroutine运行了很久但是它并没有调用另一个函数(极端的无限循环或死循环),所以如前所述它不会触发 goroutine 切换,则它不会被抢占。例如
func main() {
go func(i int) {
for {
i++
fmt.Println(i++)
}
}(0)
for{} // 主协程运行了很久。这个属于是死循环
}
1.13版本 go依赖调用函数栈增长检测代码的方式,即基于协作的抢占式调度(并非真正意义上的抢占式调度)
M 注册一个 SIGURG 信号的处理函数:sighandler。
sysmon 线程检测到执行时间过长的 goroutine 或者GC stw 时,会向相应的 M(或者说线程,每个线程对应一个 M)发送 SIGURG 信号。
M收到信号后,内核执行 sighandler 函数,确定要抢占的 goruntine(p.preempt == true && g.preempt == true && goruntine 状态 == Grunning)并检查安全性(1.G可以安全挂起并安全的扫描他的栈和寄存器并且当前被打断的位置没有打断写屏障;2.指定的G还有足够的空间来注入一个异步抢占函数调用(asyncPreempt);3.这里可以安全地和runtime进行交互,主要是确定当前G并没有持有runtime相关的锁),然后通过 pushCall 向要抢占的 goruntine 插入 asyncPreempt 函数调用。
回到当前 goroutine (要被抢占的goroutine)执行 asyncPreempt 函数,通过 mcall 切到 g0 栈执行 gopreempt_m,将当前 goroutine 插入到全局可运行队列,M 则继续寻找其他 goroutine 来运行。
被抢占的 goroutine 再次调度过来执行时,会继续原来的执行流。
主要存储引擎以及功能如下:
| 功能 | MylSAM | MEMORY | InnoDB |
|---|---|---|---|
| 存储限制 | 256TB | RAM | 64TB |
| 支持事务 | No | No | Yes |
| 支持全文索引 | Yes | No | Yes |
| 支持树索引 | Yes | Yes | Yes |
| 支持哈希索引 | No | Yes | Yes |
| 支持数据缓存 | No | N/A | Yes |
| 支持外键 | No | No | Yes |
MySQL5.5之前,默认存储引擎是MylSAM,5.5之后变成了InnoDB。
InnoDB支持的哈希索引是自适应的,InnoDB会根据表的使用情况自动为表生成哈希索引,不能人为干预是否在一张表中生成哈希索引。
MySQL 5.6开始InnoDB支持全文索引。
大多数情况下,使用默认的InnoDB就够了。如果要提供提交、回滚和恢复的事务安全(ACID 兼容)能力,并要求实现并发控制,InnoDB 就是比较靠前的选择了。
如果数据表主要用来插入和查询记录,则 MyISAM 存储引擎提供较高的处理效率。
如果只是临时存放数据,数据量不大,并且不需要较高的数据安全性,可以选择将数据保存在内存的 MEMORY 引擎中,MySQL 中使用该引擎作为临时表,存放查询的中间结果。
使用哪一种引擎可以根据需要灵活选择,因为存储引擎是基于表的,所以一个数据库中多个表可以使用不同的引擎以满足各种性能和实际需求。使用合适的存储引擎将会提高整个数据库的性能。
传统的查询方法,是按照表的顺序遍历的,不论查询几条数据,MySQL需要将表的数据从头到尾遍历一遍。
在我们添加完索引之后,MySQL一般通过 BTREE 算法生成一个索引文件,在查询数据库时,找到索引文件进行遍历,在比较小的索引数据里查找,然后映射到对应的数据,能大幅提升查找的效率。
和我们通过书的目录,去查找对应的内容,一样的道理。

推荐阅读:
看到这里有启发,欢迎点个在看或者赞呀
原文作者:皇子谈技术;如有侵权,请联系管理员删除。