字节跳动-后端-质量技术组 一面凉经 Golang
承接上条动态,写了篇万字面经 含可背诵内容 前面是问题,后面是问题+GPT的回答
by 双非&&Golang技术栈 一条路走到黑的,不会算法,略懂工程的普通大学生。
一个晴天霹雳是,面试官拿到的是我4月的简历,但现在是7月底了 我多了一个实习,删除了一段项目经历。
由于面试官看不到最新的简历,在飞书的面试上也没有办法将简历更新后发给面试官,在自我介绍我讲了最近的实习经历和最新的获奖情况后,面试官也没有任何表现。即,面试官没有询问实习的东西 太惨了
字节跳动-后端-质量技术组 一面凉经
O、惊喜又神秘的来电
六月的惊喜帖
七月的催命符
我的准备
一、自我介绍 5m
二、项目并穿插八股 45m
2.1项目部分:略
2.2八股部分(根据各板块做了划分)关系数据库
⭐MySQL和SQLite的主要区别:
⭐如果一段SQL执行缓慢,你该如何排查
⭐MySql有哪些索引类型?
⭐MySQL有哪几个数据库引擎,它们的主要区别是什么?
⭐悲观锁和乐观锁的区别非关系数据库
⭐Redis为什么快?
⭐Redis如何保证断电后数据不会丢失?如何做到数据高可用且避免不一致问题?
⭐缓存雪崩、击穿、穿透和解决办法?
⭐简要介绍一下gRPC
⭐gRPC的文件是什么后缀(格式)
⭐gRPC的代码格式是什么样的?支持定义默认值吗?定义数组的关键字是什么?
⭐除了gRPC你还用过哪些RPC技术栈,你所知道的RPC框架有哪些?
⭐QUIC相对于HTTP2有哪些重大变化?
⭐Python 和 Go 的内存管理区别
⭐slice的底层实现?
⭐slice和数组的区别?
⭐slice的扩容机制?
⭐slice是线程安全的吗?
⭐map是线程安全的吗?如何实现一个线程安全的map
⭐channel的底层实现原理
⭐channel发送数据和接收数据的过程?
⭐defer的作用
⭐defer的底层原理
⭐如果在匿名函数内panic了,在匿名函数外的defer是否会触发panic-recover?反之在匿名函数外触发panic,是否会触发匿名函数内的panic-recover?
⭐简单介绍下GMP模型
⭐简单介绍一下Golang的GC
⭐(给了一段代码,介绍一下是干嘛的)
三、三道代码手撕 25分钟
⭐lc206.反转链表 秒了
⭐lc1143.最长公共子序列 思路对了,中间过程也对 差输出部分没写出来...
四、反问
O、惊喜又神秘的来电
六月的惊喜帖
这个应该是暑期实习被捞起来的。
6月21号的下午,正在上班摸鱼的我突然接到了一个010开头的电话
em~
hr: “同学你好,是xxx吗?我是字节跳动的hr,你现在还在找暑期实习吗?”
我:“我现在暂时不找了”。
hr:“是已经找到了吗?你对我们公司有意向吗?”
我:“有,我非常向往能前往贵司实习和工作。”
hr:“好的,我这边很高兴的告知你你的简历初筛已经通过了,但由于现在我们不招暑期实习了,在8月6号前给你面试都算提前批,给你一个月准备时间,你看可以吗?”
我:“可以” hr:“那我就给你安排7月26号左右,这几天会有邮件告知给你,你确认一下”
然后我去了官网,看到的却是所有的流程都已终止,我怀揣着忐忑的心发誓要努力学习,一个月冲刺字节,同时也记住了他的话,这几天 和7月26号左右
然而,过去了一周我也没收到邮件,这让我又逐渐怠慢了下来,之前1天一题的lc,慢慢变成了三天一题。
七月的催命符
在7月25号的时候,我再次收到了010开头的神秘电话,hr说第二天下午2点面试 好吧 就很突然的,但也是有预料的。
本来是打算躺平了,毕竟24小时+上班根本来不及准备。
好在过了一小时,hr又说面试官没有时间 改到了周一下午2点,给予了接近3天的缓冲😂 虽然没有什么用,但还是感谢给了临阵磨枪的机会
我的准备
在这个周末,我放弃掉了娱乐的时间,全力冲刺,拾起遗忘的记忆,恶补没有根基的算法。
竟然让我真用了两天刷完了lc hot 100 的前60,以及额外的代码随想录上的不少题目
拿下了代码随想录左栏二叉树及前面所有的算法题,动态规划前五。
重拾为备战暑期实习准备的八股,发现没有遗忘过多的东西,还好。
一、自我介绍 5m
二、项目并穿插八股 45m
2.1项目部分:略
由于准备充分,面试官对项目的拷打没有出现影响打分的bug,我还是相当满意的。 没有出现被打懵的情况
不得不说,好的项目真的很重要啊,特别是有创新的,自己写的,原创的。
2.2八股部分(根据各板块做了划分)
关系数据库
⭐MySQL和SQLite的主要区别:
MySQL和SQLite都是关系型数据库,但它们在设计理念、应用场景和功能特性上存在显著差异。
1. 设计理念与架构
- MySQL:客户端/服务器架构:MySQL是典型的客户端/服务器模式,数据库引擎独立运行,客户端通过网络连接访问数据库。多用户并发访问:MySQL设计之初就考虑到了多用户并发访问的需求,具有较强的并发处理能力。
- SQLite:嵌入式数据库:SQLite将数据库嵌入到应用程序中,不需要独立的数据库服务器进程。单进程访问:SQLite通常只允许一个进程访问数据库,不适合高并发环境。
2. 应用场景
- MySQL:大型应用:适用于高并发、大数据量的Web应用、企业级应用等。复杂查询:支持复杂的SQL查询和事务处理。分布式部署:可以部署在多个服务器上,实现分布式数据库。
- SQLite:小型应用:适合小型桌面应用、移动应用、嵌入式系统等。数据量较小:适用于数据量较小、对性能要求不高的场景。离线应用:由于SQLite是嵌入式的,因此可以用于离线应用。
3. 功能特性
- MySQL:功能丰富:支持丰富的SQL语句、存储过程、触发器等。高性能:通过优化器、索引等技术,提供高效的数据访问。高可用性:支持主从复制、故障转移等高可用性特性。
- SQLite:轻量级:占用资源少,启动速度快。易于使用:不需要复杂的配置,易于集成到应用程序中。自包含:将数据库文件作为单个文件存储。
4. 性能对比
- SQLite在小型数据集上的读写速度通常比MySQL快。
- MySQL在处理大规模数据和高并发访问时具有更好的性能。
总结
选择哪个数据库取决于你的具体需求:
- 如果需要处理大量数据、高并发访问,并且对数据库的可靠性要求较高,那么MySQL是一个不错的选择。
- 如果需要一个轻量级的数据库,用于小型应用或嵌入式系统,并且对性能要求不高,那么SQLite是一个很好的选择。
总结来说,MySQL更适合大型复杂的应用,而SQLite更适合小型简单的应用。
⭐如果一段SQL执行缓慢,你该如何排查
当MySQL的一条SQL语句执行缓慢时,通常会严重影响数据库的整体性能。下面就来详细介绍一下如何排查和优化慢查询。
1. 开启慢查询日志
- 配置参数:
- 日志位置: 查看MySQL配置文件(如my.cnf)中的slow_query_log_file参数,找到日志保存路径。
2. 分析慢查询日志
- 查找慢SQL: 根据设置的long_query_time阈值,在慢查询日志中查找执行时间过长的SQL语句。
- 查看执行计划:使用命令分析慢SQL的执行计划,了解MySQL是如何执行这条语句的。
- 关注以下字段:type:表示MySQL在表中找到所需行的方式,如ALL(全表扫描)、index(使用索引)、range(索引范围扫描)等。possible_keys:表示MySQL能使用的索引。key:表示MySQL实际使用的索引。rows:MySQL估计要读取的行数。Extra:包含一些额外的信息,如Using filesort、Using temporary等。
3. 优化慢查询
- 创建索引: 如果查询涉及到大量的全表扫描,可以考虑为经常查询的列创建索引。但索引过多也会影响插入、更新等操作的性能,需要权衡。
- 优化SQL语句:避免使用SELECT *,只查询需要的列。避免在索引列上使用函数或运算符。尽量使用连接代替子查询。避免在WHERE子句中使用!=或<>操作符。
- 调整数据库参数:增加MySQL的缓存区大小。调整innodb_buffer_pool_size等参数。
- 优化表结构:分区表:对于大表可以考虑分区。垂直分表:将一个表拆分成多个表,减少单表数据量。
4. 其他排查方法
- 查看进程列表: 使用SHOW PROCESSLIST命令查看当前正在执行的SQL语句,找出占用资源较多的进程。
- 检查锁: 如果出现锁等待的情况,可以使用SHOW INNODB STATUS命令查看锁信息。
- 监控数据库性能: 使用MySQL提供的性能监控工具或第三方监控工具,监控数据库的CPU、内存、I/O等资源使用情况。
5. 常见慢查询原因及优化建议
- 索引问题: 缺少索引、索引失效、索引选择不当。
- SQL语句问题: 写法不规范、逻辑复杂、查询条件不合理。
- 数据库参数问题: 参数设置不合理导致性能瓶颈。
- 硬件资源问题: 磁盘I/O瓶颈、内存不足。
⭐MySql有哪些索引类型?
对 就是这个问题,其实mysql索引很多,我只答了按字段特性分类的,面试官明显不太满意。
按数据结构分类
- B+树索引: MySQL中使用最广泛的索引类型,适用于范围查询、排序等操作。它将数据按照B+树的结构存储,通过多次查找叶子节点来定位数据。
- 哈希索引: 基于哈希表的索引,仅适用于等值查询,不支持范围查询。查询速度非常快,但不能用于排序。
- 全文索引: 用于全文搜索,能够快速查找文本中的关键词。
按物理存储分类
- 聚簇索引: 数据本身按照索引的顺序存储,一个表只能有一个聚簇索引。通常主键索引就是聚簇索引。
- 二级索引: 非聚簇索引,索引和数据是分开的,通过索引找到行记录的地址,然后根据地址找到数据。
按字段特性分类
- 主键索引: 唯一标识一条记录,不允许为空值。
- 普通索引: 允许重复值和空值,用于加速对列的查询。
- 唯一索引: 索引列的值必须唯一,但允许空值。
- 组合索引: 在多个列上创建的索引,可以提高多列查询的效率。
- 全文索引: 用于全文搜索,能够快速查找文本中的关键词。
按索引的常规功能分类
- 唯一索引: 要求索引列的所有值都只能出现一次,即必须唯一。
- 普通索引: 仅用来提高查询速度,没有其他特性。
- 空间索引: 使用R树,用于索引多维数据。
⭐MySQL有哪几个数据库引擎,它们的主要区别是什么?
MySQL提供了多种存储引擎,每种引擎都有其独特的特点和适用场景。下面我们来详细介绍一下MySQL常见的存储引擎:
MySQL常见的存储引擎
- InnoDB特点: 事务安全(支持ACID特性)、支持外键、支持行级锁、支持MVCC(多版本并发控制)、支持崩溃恢复。适用场景: 绝大多数在线事务处理(OLTP)系统,要求高并发、高可靠性,以及需要事务支持的场景。它是MySQL 5.5版本以后的默认存储引擎。
- MyISAM特点: 访问速度快,占用空间小,不支持事务、不支持外键、不支持行级锁。适用场景: 适合以读操作为主,对事务完整性要求不高的应用,如数据仓库、日志系统等。
- MEMORY(HEAP)特点: 将数据全部存储在内存中,访问速度极快,但数据不持久化,重启数据库后数据丢失。适用场景: 用于临时表、缓存等,对数据安全性要求不高的场景。
⭐悲观锁和乐观锁的区别
悲观锁(Pessimistic Lock)
- 核心思想: 悲观锁认为并发冲突是一种常态,因此在数据被访问之前,就先对数据进行加锁,以防止其他线程同时修改数据。
- 实现方式:数据库层面:通过数据库提供的锁机制(如行锁、表锁)实现。代码层面:使用synchronized关键字或Lock类实现。
- 特点:安全性高:能有效防止数据并发修改导致的数据不一致问题。并发度低:由于加锁机制,会影响并发性能,尤其在高并发场景下。可能导致死锁:如果多个线程互相持有锁并等待对方释放锁,就会发生死锁。
乐观锁(Optimistic Lock)
- 核心思想: 乐观锁认为并发冲突是一种小概率事件,因此在数据被访问和修改之前,不会加锁,而是在提交更新时,才会去检查在此期间是否有其他线程修改了数据。
- 实现方式:版本号机制:给每个数据版本号,更新时比较版本号。CAS(Compare And Swap)机制:比较并交换,通过硬件指令实现原子操作。
- 特点:并发度高:没有锁的开销,并发性能高。可能出现ABA问题:如果一个变量V,先被修改为W,然后再修改回V,那么使用CAS操作时会误认为V没有被修改过。
两者对比
非关系数据库
⭐Redis为什么快?
基于内存操作:Redis的绝大部分操作在内存里就可以实现,数据也存在内存中,与传统的磁盘文件操作相比减少了IO,提高了操作的速度。
高效的数据结构:Redis有专门设计了STRING、LIST、HASH等高效的数据结构,依赖各种数据结构提升了读写的效率。
采用单线程:单线程操作省去了上下文切换带来的开销和CPU的消耗,同时不存在资源竞争,避免了死锁现象的发生。
I/O多路复用:采用I/O多路复用机制同时监听多个Socket,根据Socket上的事件来选择对应的事件处理器进行处理。
⭐Redis如何保证断电后数据不会丢失?如何做到数据高可用且避免不一致问题?
Redis数据持久化
Redis默认情况下是内存数据库,数据是存储在内存中的。为了防止断电或其他意外情况导致数据丢失,Redis提供了两种持久化机制:
- RDB(Redis DataBase):原理: 将Redis在某个时间点的数据(快照)以二进制形式保存到硬盘中。触发方式:手动触发:使用SAVE或BGSAVE命令。自动触发:配置Redis,在一定时间内有N多条数据被修改时自动触发。优点:文件恢复速度快,适用于数据恢复。配置简单。缺点:数据可能丢失:如果在两次RDB快照之间数据发生变化,而没有来得及保存,那么发生故障时会丢失部分数据。
- AOF(Append Only File):原理: 将所有的写操作命令以Redis协议的格式追加到一个文件中。触发方式:每秒同步:每秒将缓冲区中的数据写入AOF文件一次。每修改同步:每次写入都同步到AOF文件。同步关闭:在关闭服务器时才写入AOF文件。优点:数据安全性高,数据丢失的概率较低。支持数据追加,效率高。缺点:AOF文件可能会变得很大,影响性能。文件同步频率越高,性能影响越大。
建议:
- 同时开启RDB和AOF: RDB用于快速恢复数据,AOF用于保证数据不丢失。
- 配置合理的RDB保存策略: 根据业务需求设置RDB保存的时间间隔和触发条件。
- 配置合适的AOF同步策略: 在保证数据安全性的前提下,选择合适的AOF同步频率。
Redis数据高可用
- 主从复制:原理: 主节点负责写操作,从节点负责读操作,主节点将数据同步给从节点。优点:读写分离,提高性能。数据冗余,提高可用性。缺点:主节点故障时,需要手动切换。
- 哨兵模式:原理: 哨兵是Redis的监控工具,它可以监控多个Redis实例,并在主节点故障时自动进行故障转移。优点:自动故障转移,提高可用性。支持主从复制配置。缺点:配置相对复杂。
- Redis Cluster:原理: 将数据分片存储在多个节点上,每个节点负责一部分数据。优点:线性扩展,提高性能。高可用性。缺点:配置复杂,数据迁移成本高。
如何避免数据不一致问题:
- 主从复制一致性:部分同步:主节点写完数据后立即同步到从节点。全同步:主节点收到所有从节点的ack确认后才写入数据。
- 哨兵模式故障转移:哨兵会选择一个从节点作为新的主节点,并进行数据同步。
- Redis Cluster数据一致性:使用一致性哈希算法来分配数据。支持故障转移和数据迁移。
总结
为了保证Redis的数据高可用性和数据一致性,可以采取以下措施:
- 开启持久化: RDB和AOF配合使用,保证数据安全。
- 主从复制: 实现读写分离,提高性能。
- 哨兵模式: 自动故障转移,提高可用性。
- Redis Cluster: 线性扩展,提高性能和可用性。
选择合适的方案时,需要综合考虑以下因素:
- 数据量: 数据量大可以选择Redis Cluster。
- 性能要求: 对性能要求高可以选择主从复制或Redis Cluster。
- 可用性要求: 对可用性要求高可以选择哨兵模式或Redis Cluster。
⭐缓存雪崩、击穿、穿透和解决办法?
缓存雪崩
当大量缓存数据在同一时间过期或者 Redis 故障宕机时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力增加,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃。
解决方法
- 大量数据同时过期均匀设置过期时间:避免将大量的数据设置成同一个过期时间。互斥锁:当业务线程在处理用户请求时,如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存。未能获取互斥锁的请求等待锁释放后重新读取缓存,或者返回空值或者默认值。双key策略:使用两个key,一个是主key,设置过期时间,一个是备key,不会设置过期,key不一样,但是value值是一样。当业务线程访问不到主key的缓存数据时,就直接返回备key的缓存数据,然后在更新缓存的时候,同时更新主key和备key的数据。后台更新缓存:业务线程不再负责更新缓存,缓存也不设置有效期,而是让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新。
- Redis故障宕机服务熔断或请求限流机制:启动服务熔断机制,暂停业务应用对缓存服务的访问,直接返回错误,所以不用再继续访问数据库,保证数据库系统的正常运行,等到 Redis 恢复正常后,再允许业务应用访问缓存服务。服务熔断机制是保护数据库的正常允许,但是暂停了业务应用访问缓存服系统,全部业务都无法正常工作。也可以启用请求限流机制,只将少部分请求发送到数据库进行处理,再多的请求就在入口直接拒绝服务。构建高可靠集群:通过主从节点的方式构建 Redis 缓存高可靠集群。如果 Redis 缓存的主节点故障宕机,从节点可以切换成为主节点,继续提供缓存服务,避免了由于 Redis 故障宕机而导致的缓存雪崩问题。
#缓存击穿
如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮。
解决方案:
- 互斥锁方案:保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
- 不给热点数据设置过期时间:由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间。
#缓存穿透
当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。
解决方案
- 非法请求的限制:当有大量恶意请求访问不存在的数据的时候会发生缓存穿透,可以在 API 入口处判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。
- 缓存空值或者默认值:当线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。
- 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在:可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在。
计算机网络
⭐简要介绍一下gRPC
gRPC是一个高性能、开源的远程过程调用(RPC)框架,由Google开发。它可以让你像调用本地对象一样,直接调用其他机器上的服务方法。这使得构建分布式应用变得更加简单高效。
gRPC的特点:
- 高性能: gRPC基于HTTP/2协议,具有多路复用、头部压缩、双向流等特性,使得通信效率更高。
- 语言无关: gRPC支持多种编程语言,如Go、Python、Java、C#等,方便不同语言的团队协作。
- 接口定义语言: gRPC使用Protocol Buffers (Protobuf)作为接口定义语言,定义服务、方法、请求和响应的结构。Protobuf是一种高效的序列化格式,比JSON更轻量级。
- 双向流: gRPC支持双向流通信,客户端和服务器可以同时发送和接收数据,实现实时通信。
- 服务端流和客户端流: gRPC支持服务端流和客户端流,可以实现更灵活的通信模式。
gRPC的优势:
- 简化分布式系统开发: gRPC提供了一种统一的RPC框架,简化了分布式系统的开发。
- 提高系统性能: gRPC的高性能特性可以提高系统的响应速度。
- 支持多种语言: gRPC的多语言支持使得开发团队可以自由选择开发语言。
- 良好的社区支持: gRPC拥有一个活跃的社区,提供了丰富的文档和示例。
gRPC的适用场景:
- 微服务架构: gRPC非常适合构建微服务架构,不同服务之间可以通过gRPC进行通信。
- 实时通信: gRPC的双向流特性使得它非常适合实时通信场景,如聊天应用、游戏等。
- 高并发场景: gRPC的高性能特性使得它非常适合高并发场景。
⭐gRPC的文件是什么后缀(格式)
*.proto
⭐gRPC的代码格式是什么样的?支持定义默认值吗?定义数组的关键字是什么?
不支持定义默认值,为空时取内置默认 比如int是0,string是“”
关键字:repeated
这个地方 我被我自己气笑了
本来背的很熟的grpc的特性,被面试官反问:你确定是这个吗?我:不确定 面试官:哦哦,好的,了解了
⭐除了gRPC你还用过哪些RPC技术栈,你所知道的RPC框架有哪些?
我把能叫出名字的都答了,腾讯的,阿里的,标准rpc,facebook的....
但我知道面试官想让我说字节的cloudwego,我知道字节有啊,但我一时想不起来名字 哭了
⭐QUIC相对于HTTP2有哪些重大变化?
QUIC是个新方向,估计以后用的会越来越多
1. 底层协议:
- HTTP/2基于TCP: HTTP/2是基于TCP协议的,而TCP是一种面向连接的协议,存在连接建立慢、拥塞控制复杂等问题。
- QUIC基于UDP: QUIC直接基于UDP协议,UDP是一种无连接的协议,具有连接建立快、头部开销小等优点。
2. 连接管理:
- HTTP/2的多路复用: HTTP/2引入了多路复用,多个请求可以共用一个TCP连接,提高了网络利用率。
- QUIC的多路复用: QUIC的复用粒度更细,每个请求都可以有独立的流,并且可以同时进行多个连接,进一步提升了并发能力。
3. 连接迁移:
- HTTP/2没有连接迁移: HTTP/2的连接一旦建立,就固定在当前网络路径上,无法进行迁移。
- QUIC的连接迁移: QUIC支持连接迁移,当网络条件发生变化时,可以将连接平滑地迁移到新的网络路径上,提高了连接的稳定性。
4. 拥塞控制:
- HTTP/2的拥塞控制: HTTP/2沿用了TCP的拥塞控制算法,在复杂网络环境下可能不够灵活。
- QUIC的拥塞控制: QUIC采用了更灵活的拥塞控制算法,可以根据网络条件动态调整发送速率,提高了网络利用率。
5. 加密:
- HTTP/2的加密: HTTP/2的加密是可选的,需要额外的TLS握手。
- QUIC的加密: QUIC的加密是强制的,并且在连接建立的早期阶段就完成了加密,提高了安全性。
6. 头部压缩:
- HTTP/2的头部压缩: HTTP/2使用HPACK算法对头部进行压缩,减少了头部开销。
- QUIC的头部压缩: QUIC也采用了头部压缩技术,但压缩算法可能有所不同。
7. 其他改进:
- 更快的连接建立: QUIC的连接建立速度比HTTP/2快得多。
- 更低的延迟: QUIC的延迟更低,尤其是在移动网络环境下。
- 更好的错误恢复: QUIC的错误恢复机制更完善,可以更快地从丢包中恢复。
编程语言部分
golang底层原理被打穿😂
面试官:你更熟悉java/python/golang/c++这些哪一个
我:我第一语言是golang,而python对于每个程序员来说都应该要掌握。我选择python作为第二语言。如果最好,我们尽量按这样选择:带GC的静态编程语言学一门(如Golang,Rust,Java),不带GC的静态编程语言学一门(如C++),多门脚本语言(如python/shell/lua/JavaScript等)
⭐Python 和 Go 的内存管理区别
Python 和 Go 作为两种流行的编程语言,在内存管理方面有着显著的不同。这主要源于它们在语言设计、运行时环境以及垃圾回收机制上的差异。
1. 内存管理方式
- Python:引用计数: Python 主要采用引用计数的方式来管理内存。每个对象都有一个引用计数器,当引用计数为 0 时,对象就会被销毁。标记-清除: 为了解决循环引用问题,Python 引入了标记-清除算法,周期性地扫描所有对象,标记可达对象,然后回收不可达对象。分代回收: Python 将对象分为不同的代,新创建的对象属于年轻代,存活时间长的对象属于年老代。不同代的回收频率不同,以优化回收效率。
- Go:三色标记: Go 采用三色标记法进行垃圾回收。将对象分为白色(未访问)、灰色(正在访问)和黑色(访问完毕)三种颜色,通过标记和扫描的方式来确定可达对象。并发标记清除: Go 的垃圾回收器是并发执行的,可以与应用程序并发运行,减少程序的停顿时间。
2. 内存分配
- Python:Python 的内存分配器相对简单,通常使用一个较大的内存池,每次分配内存时从池中切割一块。
- Go:Go 的内存分配器相对复杂,采用了更细粒度的内存管理。它将内存分为多个 span,每个 span 管理一定大小的内存块。Go 的内存分配器还支持 tcmalloc,可以提高内存分配的效率。
3. 垃圾回收触发
- Python:Python 的垃圾回收器会根据内存使用情况和对象创建速率来决定是否触发垃圾回收。
- Go:Go 的垃圾回收器会根据一系列的指标来触发,包括堆的大小、堆增长速率、并发数等。
4. 内存管理特点对比
⭐slice的底层实现?
切片的底层是一个结构体,对应三个参数,一个是unsafe.Pointer指针,指向一个具体的底层数组,一个是cap,切片的容量,一个是len,切片的长度。
因为切片是基于数组实现,所以它的底层的内存是连续分配的,效率非常高,可以通过索引获得数据。切片本身并不是动态数组或者数组指针,而是设定相关属性,将数据读写操作限定在指定的区域内。切片本身是一个只读对象,其工作机制类似数组指针的一种封装。
如果make函数初始化了一个太大的切片,该切片就会逃逸到堆区;如果分配了一个比较小的切片,就会被分配到栈区,切片大小的临界值默认为64KB,因此make([]int64, 1023) 和 make([]int64, 1024) 是完全不同的内存布局。
⭐slice和数组的区别?
切片是指针类型,数组是值类型
传递数组是通过拷贝的方式,传递切片是通过传递引用的方式。
数组的长度固定,而切片可以进行动态扩容
数组是一组内存空间连续的数据,一旦初始化长度大小就不会再改变,切片的长度可以进行扩展,当切片底层的数组容量不够时,切片会创建新的底层数组。
切片比数组多一个属性容量(cap)
⭐slice的扩容机制?
扩容主要分为两个过程:第一步是分配新的内存空间,第二步是将原有切片内容进行复制。分配新空间时候需要估计大致容量,然后再确定容量。
根据该切片当前容量选择不同的策略:
- 如果期望容量大于当前容量的两倍,就会使用期望容量
- 如果当前切片的长度小于 1024,容量就会翻倍
- 如果当前切片的长达大于 1024,每次扩容 25% 的容量,直到新容量大于期望容量
- 在进行循环1.25倍计算时,最终容量计算值发生溢出,即超过了int的最大范围,则最终容量就是新申请的容量
对于切片的扩容
- 当切片比较小的,采用较大的扩容倍速进行扩容,避免频繁扩容,从而减少内存分配的次数和数据拷贝的代价
- 当切片较大的时,采用较小的扩容倍速,主要避免空间浪费
⭐slice是线程安全的吗?
不是。slice底层结构并没有使用加锁等方式,不支持并发读写,所以并不是线程安全的,使用多个goroutine对类型为slice的变量进行操作,每次输出的值大概率都不会一样,与预期值不一致; slice在并发执行中不会报错,但是数据会丢失。
⭐map是线程安全的吗?如何实现一个线程安全的map
- 加读写锁
- 分片加锁
- sync.Map
⭐channel的底层实现原理
Go 语言中的 channel 是一个非常重要的并发编程原语,用于在 goroutine 之间传递数据和同步。它的底层实现非常巧妙,保证了并发安全性和高效性。
Channel 的数据结构
- hchan 结构体:这是 channel 的核心数据结构,包含了以下主要字段:buf: 缓冲区,用于存储待发送或接收的数据。qcount: 当前缓冲区中的元素个数。dataqsize: 缓冲区的容量。lock: 互斥锁,用于保护 channel 的并发访问。sendx: 发送端索引。recvx: 接收端索引。waitq: 等待队列,用于存放阻塞的 goroutine。
Channel 的操作
- 创建 channel:分配 hchan 结构体内存。根据容量初始化缓冲区。初始化其他字段。
- 发送数据:首先获取 channel 的锁。如果缓冲区未满,直接将数据写入缓冲区,并更新 sendx。如果缓冲区已满,将当前 goroutine 挂到 waitq 队列的发送端,并释放锁,让出 CPU。等待接收端取走数据后,被唤醒继续执行。
- 接收数据:首先获取 channel 的锁。如果缓冲区非空,直接从缓冲区读取数据,并更新 recvx。如果缓冲区为空,将当前 goroutine 挂到 waitq 队列的接收端,并释放锁,让出 CPU。等待发送端写入数据后,被唤醒继续执行。
- 关闭 channel:将 channel 的状态标记为关闭。唤醒所有在该 channel 上阻塞的 goroutine。
Channel 的特点
- 类型安全: channel 传递的数据类型是固定的。
- 同步性: 发送和接收操作会自动同步,保证数据的正确传递。
- 缓冲: 有缓冲的 channel 可以提高并发效率,无缓冲的 channel 可以实现同步。
- 关闭: 可以关闭 channel,通知其他 goroutine。
Channel 的底层实现要点
- 循环缓冲区: 缓冲区采用循环数组实现,提高了内存利用率。
- 等待队列: 等待队列使用双向链表实现,方便 goroutine 的挂起和唤醒。
- 互斥锁: 保护 channel 的并发访问,保证数据一致性。
- 非阻塞操作: 提供了非阻塞的发送和接收操作,可以避免 goroutine 被永久阻塞。
⭐channel发送数据和接收数据的过程?
channel发送数据过程:
- 检查 recvq 是否为空,如果不为空,则从 recvq 头部取一个 goroutine,将数据发送过去,并唤醒对应的 goroutine
- 如果 recvq 为空,则将数据放入到 buffer 中
- 如果 buffer 已满,则将要发送的数据和当前 goroutine 打包成 sudog 对象放入到 sendq中。并将当前 goroutine 置为 waiting 状态
channel接收数据过程:
- 检查sendq是否为空,如果不为空,且没有缓冲区,则从sendq头部取一个goroutine,将数据读取出来,并唤醒对应的goroutine,结束读取过程
- 如果sendq不为空,且有缓冲区,则说明缓冲区已满,则从缓冲区中首部读出数据,把sendq头部的goroutine数据写入缓冲区尾部,并将goroutine唤醒,结束读取过程
- 如果sendq为空,缓冲区有数据,则直接从缓冲区读取数据,结束读取过程
- 如果sendq为空,且缓冲区没数据,则只能将当前的goroutine加入到recvq,并进入waiting状态,等待被写goroutine唤醒
⭐defer的作用
⭐defer的底层原理
⭐如果在匿名函数内panic了,在匿名函数外的defer是否会触发panic-recover?反之在匿名函数外触发panic,是否会触发匿名函数内的panic-recover?
都不会:这是单调栈问题。
panic-recover机制是局部生效的。 每个函数都有自己的defer栈,只能捕获到本函数以及内部函数的panic。
panic的传播是单向的,从内向外。 外层的defer无法捕获内层的panic。
⭐简单介绍下GMP模型
⭐简单介绍一下Golang的GC
⭐(给了一段代码,介绍一下是干嘛的)
给了段golang gc 底层的代码 直接趴窝了真的
三、三道代码手撕 25分钟
一开始面试官发题目来我这一直空白加载不出来,换了两次网才好 浪费了一点时间
字节的手撕是没有任何数据结构定义的,题目都没有,要自己从0撕,面试官跟你说你要干啥 然后只有一个框,一点数据
⭐lc206.反转链表 秒了
⭐ 没看到题 面试官觉得不合适删了