腾讯互娱面经,希望别凉
面试题详解
Go 接口
接口在 Golang 中扮演着连接不同类型之间的桥梁,它定义了一组方法的集合,而不关心具体的实现。接口的作用主要体现在以下几个方面:
多态性:
接口允许不同的类型实现相同的方法,从而实现多态性。这意味着我们可以使用接口类型来处理不同的对象,而不需要关心具体的类型。
在上面的示例中,我们定义了一个 Animal 接口,它包含了一个 Sound()方法。然后,我们实现了 Dog 和 Cat 两个结构体,分别实现了 Sound()方法。通过将 Dog 和 Cat 类型赋值给 Animal 接口类型,我们可以在循环中调用 Sound()方法,而不需要关心具体的类型。这就体现了接口的多态性,不同的类型可以实现相同的接口方法。
解耦合:
接口可以将抽象与实现分离,降低代码之间的耦合度。通过定义接口,我们可以将实现细节隐藏起来,只暴露必要的方法,从而提高代码的可维护性和可读性。
在上面的示例中,我们定义了一个 Printer 接口,它包含了一个 Print()方法。然后,我们实现了 ConsolePrinter 和 FilePrinter 两个结构体,分别实现了 Print()方法。通过将不同的结构体赋值给 Printer 接口类型的变量,我们可以在主函数中调用 Print()方法,而不需要关心具体的实现。这样,我们可以根据需要轻松地切换不同的打印方式,实现了解耦合。
可扩展性:
在上面的示例中,我们定义了一个 Shape 接口,它包含了一个 Area()方法。然后,我们实现了 Rectangle 和 Circle 两个结构体,分别实现了 Area()方法。通过将不同的结构体赋值给 Shape 接口类型的切片,我们可以在循环中调用 Area()方法,而不需要关心具体的类型。这样,当我们需要添加新的形状时,只需要实现 Shape 接口的 Area()方法即可,而不需要修改已有的代码。这就实现了代码的可扩展性。
接口的应用场景
API 设计:接口在 API 设计中起到了至关重要的作用。通过定义接口,我们可以规范 API 的输入和输出,提高代码的可读性和可维护性。
单元测试:接口在单元测试中也扮演着重要的角色。通过使用接口,我们可以轻松地替换被测试对象的实现,从而实现对被测代码的独立测试。
插件系统:接口可以用于实现插件系统,通过定义一组接口,不同的插件可以实现这些接口,并在程序运行时动态加载和使用插件。
依赖注入:接口在依赖注入中也有广泛的应用。通过定义接口,我们可以将依赖对象的创建和管理交给外部容器,从而实现松耦合的代码结构。
空结构体的用途
不包含任何字段的结构体,就叫做空结构体。
空结构体的特点:
零内存占用
地址都相同
无状态
空结构体的使用场景
实现
set
集合
在 Go 语言中,虽然没有内置 Set 集合类型,但是我们可以利用 map 类型来实现一个 Set 集合。由于 map 的 key 具有唯一性,我们可以将元素存储为 key,而 value 没有实际作用,为了节省内存,我们可以使用空结构体作为 value 的值。
用于通道信号
空结构体常用于 Goroutine 之间的信号传递,尤其是不关心通道中传递的具体数据,只需要一个触发信号时。例如,我们可以使用空结构体通道来通知一个 Goroutine 停止工作:
作为方法接收器
有时候我们需要创建一组方法集的实现(一般来说是实现一个接口),但并不需要在这个实现中存储任何数据,这种情况下,我们可以使用空结构体来实现:
Go 原生支持默认参数或可选参数吗,如何实现
什么是默认参数
默认参数是指在函数调用时,如果没有提供某个参数的值,那么使用函数定义中指定的默认值。这种语言特性可以减少代码量,简化函数的使用。
在 Go 语言中,函数不支持默认参数。这意味着如果我们想要设置默认值,那么就需要手动在函数内部进行处理。
例如,下面是一个函数用于计算两个整数的和:
如果我们希望 b 参数有一个默认值,例如为 0,那么可以在函数内部进行处理:
上面的代码中,如果 b 参数没有提供值,那么默认为 0。通过这种方式,我们就实现了函数的默认参数功能。
需要注意的是,这种处理方式虽然可以实现默认参数的效果,但会增加代码复杂度和维护难度,因此在 Go 语言中不被推荐使用。
什么是可选参数
可选参数是指在函数调用时,可以省略一些参数的值,从而让函数更加灵活。这种语言特性可以让函数更加易用,提高代码的可读性。
在 Go 语言中,函数同样不支持可选参数。但是,我们可以使用可变参数来模拟可选参数的效果。
下面是一个函数用于计算任意个整数的和:
上面的代码中,我们使用...int 类型的可变参数来接收任意个整数,并在函数内部进行求和处理。
如果我们希望 b 和 c 参数为可选参数,那么可以将它们放到 nums 可变参数之后:
上面的代码中,我们首先将 a 参数赋值给 sum 变量,然后对可变参数进行求和处理。如果函数调用时省略了 nums 参数,则 sum 等于 a 的值。
需要注意的是,使用可变参数模拟可选参数的效果虽然能够实现函数的灵活性,但也会降低代码的可读性和规范性。因此在 Go 语言中不被推荐使用。
defer 执行顺序
在 Go 中,defer 语句用于延迟(defer)函数的执行,通常用于在函数执行结束前执行一些清理或收尾工作。当函数中存在多个 defer 语句时,它们的执行顺序是**“后进先出”**(Last In First Out,LIFO)的,即最后一个被延迟的函数最先执行,倒数第二个被延迟的函数次之,以此类推。
在 Go 中,defer 语句中的函数在执行时会被压入一个栈中,当函数执行结束时,这些被延迟的函数会按照后进先出的顺序执行。这意味着在函数中的 defer 语句中的函数会在函数执行结束前执行,包括在 return 语句之前执行。
协程之间信息如何同步
协程(Goroutine)之间的信息同步通常通过通道(Channel)来实现。通道是 Go 语言中用于协程之间通信的重要机制,可以安全地在不同协程之间传递数据,实现协程之间的信息同步。
一些常见的方法来实现协程之间的信息同步:
使用无缓冲通道:无缓冲通道是一种同步通道,发送和接收操作会在数据准备好之前被阻塞。通过无缓冲通道,可以实现协程之间的同步通信,确保数据的正确传递。
使用带缓冲通道:带缓冲通道允许在通道中存储一定数量的元素,发送操作只有在通道已满时才会被阻塞。通过带缓冲通道,可以实现异步通信。
使用同步原语:Go 语言中的
sync
包提供了一些同步原语,如互斥锁(Mutex
)、读写锁(RWMutex
)等,可以用于协程之间的同步访问共享资源。使用
select
语句:select
语句可以用于在多个通道操作中选择一个执行,可以实现协程之间的多路复用和超时控制。使用
context
包:context
包提供了一种在协程之间传递取消信号和截止时间的机制,可以用于协程之间的协调和同步。
GMP 模型
GM 模型开销大的原因?
最开始的是 GM 模型没有 P 的,是 M:N 的两级线程模型,但是会出现一些性能问题:
**全局队列的锁竞争。**M 从全局队列中添加或获取 G 的时候,都是需要上锁的(下图执行步骤要加锁),这样就会导致锁竞争,虽然达到了并发安全,但是性能是非常差的。
**M 转移 G 会有额外开销。**M 在执行 G 的时候,假设 M1 执行的 G1 创建了 G2,新创建的就要放到全局队列中去,但是这时有一个空闲的 M2 获取到了 G2,那么这样 G1、G2 会被不同的 M 执行,但是 M1 中本来就有 G2 的信息,M2 在 G1 上执行是更好的,而且取和放到全局队列也会来回加锁,这样都会有一部分开销。
**线程的使用效率不能最大化。**M 拿不到的时候就会一直空闲,阻塞的时候也不会切换。也就是没有 workStealing 机制和 handOff 机制。
GMP
go 生成一个协程,此时放在 P 中还是 M 中
如果在本地的队列中有足够的空间,则会直接进入本地队列等待 M 的执行;如果本地队列已经满了,则进入全局队列(在 GMP 模型中,所有的 M 都可以从全局队列中获取协程并执行)
G 阻塞,M、P 如何
当 G 因系统调用(syscall)阻塞时会阻塞 M,此时 P 会和 M 解绑即 hand off,并寻找新的 idle 的 M,若没有 idle 的 M 就会新建一个 M
Redis 与 MySQL 数据如何同步
这里提供几种方案:
**定时任务同步:**编写定时任务或脚本,定期从 MySQL 中读取数据,然后将数据同步到 Redis 中。这种方案简单直接,适用于数据量不大且同步频率不高的场景。
**使用消息队列:**将 MySQL 中的数据变更操作(如新增、更新、删除)通过消息队列(如 RabbitMQ、Kafka)发送到消息队列中,然后消费者从消息队列中读取消息,将数据同步到 Redis 中。这种方案实现了异步同步,降低了对 MySQL 的压力。
**使用数据库触发器:**在 MySQL 中设置触发器,当数据发生变更时触发触发器,将变更信息发送到消息队列或直接同步到 Redis 中。这种方案可以实现实时同步,但需要谨慎设计触发器逻辑,避免影响数据库性能。
**使用数据同步工具:**可以使用一些数据同步工具(如 Maxwell、Debezium)来实现 MySQL 和 Redis 数据的实时同步。这些工具可以监控 MySQL 数据库的变更,并将变更数据同步到 Redis 中。
**使用缓存库:**一些缓存库(如 Redisson、Lettuce)提供了与 MySQL 数据库的集成,可以通过配置简单地实现 MySQL 数据到 Redis 的同步。
Explain 的字段
explain 的用法:
返回结果:
下面对上面截图中的字段一一解释:
1、id:select 查询序列号。id 相同,执行顺序由上至下;id 不同,id 值越大优先级越高,越先被执行。
2、select_type:查询数据的操作类型,其值如下:
simple:简单查询,不包含子查询或 union
primary:包含复杂的子查询,最外层查询标记为该值
subquery:在 select 或 where 包含子查询,被标记为该值
derived:在 from 列表中包含的子查询被标记为该值,MySQL 会递归执行这些子查询,把结果放在临时表
union:若第二个 select 出现在 union 之后,则被标记为该值。若 union 包含在 from 的子查询中,外层 select 被标记为 derived
union result:从 union 表获取结果的 select
3、table:显示该行数据是关于哪张表
4、partitions:匹配的分区
5、type:表的连接类型,其值,性能由高到底排列如下:
system:表只有一行记录,相当于系统表
const:通过索引一次就找到,只匹配一行数据
eq_ref:唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常用于主键或唯一索引扫描
ref:非唯一性索引扫描,返回匹配某个单独值的所有行。用于=、< 或 > 操作符带索引的列
range:只检索给定范围的行,使用一个索引来选择行。一般使用 between、>、<情况
index:只遍历索引树
ALL:全表扫描,性能最差
6、 possible_keys:显示 MySQL 理论上使用的索引,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询实际使用。如果该值为 NULL,说明没有使用索引,可以建立索引提高性能
7、key:显示 MySQL 实际使用的索引。如果为 NULL,则没有使用索引查询
8、key_len:表示索引中使用的字节数,通过该列计算查询中使用的索引的长度。在不损失精确性的情况下,长度越短越好 显示的是索引字段的最大长度,并非实际使用长度
9、ref:显示该表的索引字段关联了哪张表的哪个字段
10、 rows:根据表统计信息及选用情况,大致估算出找到所需的记录或所需读取的行数,数值越小越好
11、filtered:返回结果的行数占读取行数的百分比,值越大越好
12、extra:包含不合适在其他列中显示但十分重要的额外信息,常见的值如下:
using filesort:说明 MySQL 会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取。出现该值,应该优化 SQL
using temporary:使用了临时表保存中间结果,MySQL 在对查询结果排序时使用临时表。常见于排序 order by 和分组查询 group by。出现该值,应该优化 SQL
using index:表示相应的 select 操作使用了覆盖索引,避免了访问表的数据行,效率不错
using where:where 子句用于限制哪一行
using join buffer:使用连接缓存
distinct:发现第一个匹配后,停止为当前的行组合搜索更多的行
Redis 过期删除策略
Redis 中提供了三种过期删除的策略
定时删除
在设置某个 key 的过期时间同时,我们创建一个定时器,让定时器在该过期时间到来时,立即执行对其进行删除的操作。
优点:
对 CPU 是友好的,只有在取出键值对的时候才会进行过期检查,这样就不会把 CPU 资源花费在其他无关紧要的键值对的过期删除上。
缺点:
如果一些键值对永远不会被再次用到,那么将不会被删除,最终会造成内存泄漏,无用的垃圾数据占用了大量的资源,但是服务器却不能去删除。
惰性删除
惰性删除,当一个键值对过期的时候,只有再次用到这个键值对的时候才去检查删除这个键值对,也就是如果用不着,这个键值对就会一直存在。
优点:
对 CPU 是友好的,只有在取出键值对的时候才会进行过期检查,这样就不会把 CPU 资源花费在其他无关紧要的键值对的过期删除上。
缺点:
如果一些键值对永远不会被再次用到,那么将不会被删除,最终会造成内存泄漏,无用的垃圾数据占用了大量的资源,但是服务器却不能去删除。
定期删除
定期删除是对上面两种删除策略的一种整合和折中
每个一段时间就对一些 key 进行采样检查,检查是否过期,如果过期就进行删除
1、采样一定个数的 key,采样的个数可以进行配置,并将其中过期的 key 全部删除;
2、如果过期 key 的占比超过可接受的过期 key 的百分比,则重复删除的过程,直到过期 key 的比例降至可接受的过期 key 的百分比以下。
优点:
定期删除,通过控制定期删除执行的时长和频率,可以减少删除操作对 CPU 的影响,同时也能较少因过期键带来的内存的浪费。
缺点:
执行的频率不太好控制
频率过快对 CPU 不友好,如果过慢了就会对内存不太友好,过期的键值对不能及时的被删除掉
同时如果一个键值对过期了,但是没有被删除,这时候业务再次获取到这个键值对,那么就会获取到被删除的数据了,这肯定是不合理的。
Redis 中过期删除策略
上面讨论的三种策略,都有或多或少的问题。Redis 中实际采用的策略是惰性删除加定期删除的组合方式。
定期删除,获取 CPU 和 内存的使用平衡,针对过期的 KEY 可能得不到及时的删除,当 KEY 被再次获取的时候,通过惰性删除再做一次过期检查,来避免业务获取到过期内容。
Redis 常用数据结构
Redis 共有 5 种基本数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。
Zset 使用场景, 具体实现
Zset 的两种实现方式:
ziplist:满足以下两个条件的时候
元素数量少于 128 的时候
每个元素的长度小于 64 字节
skiplist:不满足上述两个条件就会使用跳表,具体来说是组合了 map 和 skiplist
map 用来存储 member 到 score 的映射,这样就可以在 O(1)时间内找到 member 对应的分数
skiplist 按从小到大的顺序存储分数
skiplist 每个元素的值都是[score,value]对
skiplist 优势
skiplist 本质上是并行的有序链表,但它克服了有序链表插入和查找性能不高的问题。它的插入和查询的时间复杂度都是 O(logN)
skiplist 原理
普通有序链表的插入需要一个一个向前查找是否可以插入,所以时间复杂度为 O(N),比如下面这个链表插入 23,就需要一直查找到 22 和 26 之间。
如果节点能够跳过一些节点,连接到更靠后的节点就可以优化插入速度:
在上面这个结构中,插入 23 的过程是
先使用第 2 层链接 head->7->19->26,发现 26 比 23 大,就回到 19
再用第 1 层连接 19->22->26,发现比 23 大,那么就插入到 26 之前,22 之后
上面这张图就是跳表的初步原理,但一个元素插入链表后,应该拥有几层连接呢?跳表在这块的实现方式是随机的,也就是 23 这个元素插入后,随机出一个数,比如这个数是 3,那么 23 就会有如下连接:
第 3 层 head->23->end
第 2 层 19->23->26
第 1 层 22->23->26
下面这张图展示了如何形成一个跳表
在上述跳表中查找/插入 23 的过程为:
总结一下跳表原理:
每个跳表都必须设定一个最大的连接层数 MaxLevel
第一层连接会连接到表中的每个元素
插入一个元素会随机生成一个连接层数值[1, MaxLevel]之间,根据这个值跳表会给这元素建立 N 个连接
插入某个元素的时候先从最高层开始,当跳到比目标值大的元素后,回退到上一个元素,用该元素的下一层连接进行遍历,周而复始直到第一层连接,最终在第一层连接中找到合适的位置
使用场景:
对有序数据进行排序,例如新闻排行榜或游戏排行榜。
对数据进行分组,例如将所有评分在 3.0 到 4.0 之间的电影分为一组。
对数据进行去重,例如将所有重复的单词从文本中删除。
早日上岸!
我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。
没准能让你能刷到自己意向公司的最新面试题呢。
感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。
版权声明: 本文为 InfoQ 作者【王中阳Go】的原创文章。
原文链接:【http://xie.infoq.cn/article/1e33cb453d9fdd9ffa07c5ff2】。文章转载请联系作者。
评论