大家好,我是阳哥。专注Go语言的学习经验分享和就业辅导。
之前分享了很多 Golang 后端的大厂面经,不少同学在催更新,这篇给大家继续安排。
本文来自一位同学的投稿,面试深X服的面经汇总,前半部分主要是Go语言相关,后半部分也涉及微服务和Redis。
- slice切片扩容机制?为什么不一直用2倍扩容?
go1.18版本之后,slice扩容使用了更加平滑的方式,不再使用1024作为临界点,而是使用threshold作为临界点(threshold 设定为256)。
当slice容量 < threshold 时,每次扩容为原来的两倍。
当slice容量 > threshold 时,每次增加 (oldcap + 3*threshold)*3/4的容量。
如果每次扩容都用两倍扩容,那这是一个2的指数级增加,后面扩容会扩非常大,浪费很大的空间。
- Go内存分配机制?多级缓存?组件?(mspan/mcache/mcentral…)
Go 内存会自己管理,释放的内存不会立马归还给操作系统,而是尽量对内存进行复用,减少和操作系统的沟通(涉及到内核态和用户态的转换,消耗较大)。
Go内存管理非常高效,每一个线程M独享一个mcache,在申请内存时优先从mcache上获取,如果mcache没有足够内存,则向mcentral获取内存到mcache中,如果mcentral中也没有足够的内存则向mheap申请,如果mheap也没有足够的内存的话就向操作系统申请了(mcache->mcentral->mheap->OS)。
高效体现在从mcache获取内存和释放都是无锁的,速度很快,向mcentral和mheap申请内存虽然需要竞争锁,但是mcentral 和mheap通过 span class 进行分类(设置桶锁而不是一把大锁),锁的粒度更小,申请内存时都是通过span class 找到对应的桶获取内存,锁竞争会减少很多,性能也得到了提升。
- go垃圾回收?三色标记法?读写屏障?STW?
go1.18之后垃圾回收采用三色标记法和混合写屏障。
将对象标记为白色,灰色或黑色。
白色:不确定对象(默认色);黑色:存活对象。灰色:存活对象,子对象待处理。
标记开始时,先将所有对象加入白色集合(需要STW)。首先将根对象标记为灰色,然后将一个对象从灰色集合取出,遍历其子对象,放入灰色集合。同时将取出的对象放入黑色集合,直到灰色集合为空。最后的白色集合对象就是需要清理的对象。
这种方法有一个缺陷,如果对象的引用被用户修改了,那么之前的标记就无效了。因此Go采用了写屏障技术,当对象新增或者更新会将其着色为灰色。
一次完整的GC分为四个阶段:
基于插入写屏障和删除写屏障在结束时需要STW来重新扫描栈,带来性能瓶颈。混合写屏障分为以下四步:
总而言之就是确保黑色对象不能引用白色对象,这个改进直接使得GC时间从 2s降低到2us。
- channel使用场景?CSP思想?优势?
在并发编程中,两个goroutine之间使用channel进行通信。
CSP思想是指通过通信来共享内存,而不是通过共享内存来实现通信。
CSP思想优势是使用非常灵活,缺点是容易造成死锁。
- go的锁有哪些?互斥锁的读写锁的区别?
互斥锁和读写锁。
互斥锁是有一个goroutine获取到互斥锁后,其他goroutine必须要等待锁被释放才能获取到锁。
而读写锁是可以允许多个goroutine获取到读锁执行读操作,但是写锁被获取了就和互斥锁一样了,其他goroutine必须等待写锁释放才能获取到读锁或者写锁。
- sync包还有什么?sync.map介绍一下?如何保证并发安全?
sync.waitgroup, sync.map ,sync.Lock, sync.RWLock,sync.Pool…
sync.map 底层有两个map,一个是read,一个是dirty, 在读操作时优先在read中查找,read没有就去dirty中查找,写操作时如果read中有则利用CAS机制尝试更新value值,read中没有则写入 dirty 中。在 misses >= len(dirty)时,同步read 和 dirty的数据。
在read中不需要加锁,在dirty中需要加锁
sync.map通过read和dirty实现读写分离来减少锁时间来提高并发效率
- sync.Pool?了解线程池吗?优势亮点?未工作的线程如何等待?协程池?
sync.Pool 其实就是一个线程安全的对象池,用于保存和复用临时对象,在大批量申请和释放相同类型的临时对象时使用 sync.Pool 可以减少很多内存分配和回收操作,减小GC压力。
线程池是一种并发编程的技术,可以管理和复用线程,提供一种高效的方式来处理并发。
协程池和线程池基本差不多,可以更好地管理和复用协程,提高系统的性能和资源利用率。
- 防止Go协程泄露/未关闭?(waitgroup,context)
通过管道channel通知关闭,使用waitgroup监控协程全部退出,使用contex上下文来设置超时或者手动cancle关闭协程
select的用法?执行顺序?
select{
case <-ctx.Done():
return
case <-
default:
}
执行顺序是随机的
- 什么可以作为map的键?结构体可以吗?
实现了 == 操作的可以比较的就可以作为map的键(基本数据类型、数组等),结构体中如果所有字段都可以比较那么久可以作为键,否则不行。
- 微服务之间通信方式 rpc、grpc、http等
GRPC是一种高性能、开源的远程过程调用(RPC)框架,由Google开发并基于HTTP/2协议实现。它允许在不同的计算机之间进行跨语言和跨平台的通信,使得构建分布式系统变得更加简单和高效。
GRPC使用Protocol Buffers(简称Protobuf)作为默认的序列化机制,而不是使用JSON。
Protobuf是一种轻量级的数据交换格式,具有以下优势:
相比之下,JSON是一种常用的文本格式,具有以下特点:
总的来说,GRPC使用Protobuf作为默认的序列化机制,相比于JSON,Protobuf在性能、可读性和跨语言支持方面具有优势。然而,选择使用GRPC还是JSON取决于具体的应用场景和需求。
介绍一下Gorm优势?
简单易用、支持多种数据库、自动迁移、支持事务、
1.常见数据结构
答:(字符串、set、zset、hash、list…)
hash和zset底层使用 skiplist 和 ziplist,同时满足两个条件时使用 ziplist,否则 skiplist
list 底层使用了快表(quicklist),quickList 本质上是 zipList 和 linkedList 的混合体。其将 linkedList 按段切分,每一段使用 zipList 来紧凑存储若干真正的数据元素,多个 zipList 之间使用双向指针串接起来。对于每个zipList 中最多可存放多大容量的数据元素,在配置文件中通过 list-max-ziplist-size 属性可以指定。
2.zset介绍一下?zset底层借助那些数据结构实现?跳表介绍一下?查找过程?复杂度?
zset是redis中的有序集合,借助了跳表和哈希表(哈希表存储成员和对应的分数)实现有序集合,跳表是一种有序的链表结构,它通过在每个节点中维护多个指针,使得在查找和插入操作时可以跳过部分节点,从而提高了查找的效率。跳表的高度是通过随机函数决定的,因此在平均情况下,查找的时间复杂度为O(log n),其中n是跳表中的节点数量。
3.set介绍?
set 底层使用哈希表。
4.压缩列表介绍?
压缩列表的设计目标是在尽可能节省内存的同时,提供高效的插入、删除和访问操作。它通过使用连续的内存块来存储数据,减少了指针和额外的元数据开销,从而节省了内存空间。
5.渐进式rehash?
渐进式rehash是Redis在进行哈希表扩容时采用的一种策略。当哈希表需要扩容时,Redis会创建一个新的哈希表,并将原有哈希表中的数据逐步迁移到新的哈希表中。这个过程是逐步进行的,每次只迁移一小部分数据,以避免在一次性迁移过程中对系统性能造成较大的影响。
渐进式rehash的过程如下:
通过渐进式rehash,Redis可以在不中断服务的情况下进行哈希表的扩容。这种方式可以避免在一次性迁移过程中对系统性能造成较大的影响,因为每次只迁移一小部分数据。同时,渐进式rehash还可以保持对原有哈希表的访问能力,确保在迁移过程中数据的一致性。
下面的面经同样精彩,希望对大家找工作有帮助:
噢耶!字节跳动后端Offer,拿到了!
一天约了4个面试,复盘一下面试经历和薪资范围
避免失业和35岁危机,把这份百度3面的面经分享出来
2023最新社招面经分享:字节 米哈游 富途 猿辅导
也欢迎关注我的公众号: 程序员升职加薪之旅
微信号:wangzhongyang1993
也欢迎大家关注我的账号,点赞、留言、转发。你的支持,是我更文的最大动力!