前言:
为了方便自己复习和巩固,基础知识,整理了这个面试题汇总
go中底层数据结构是由一个array指针指向底层数组,len表示数组长度,cap表示切片容量。切片中的元素存放在一块内存地址连续的区域,使用索引可以快速检索到指定位置的元素;切片长度和容量是可变的,在使用过程中可以根据需要进行扩容。slice 的主要实现是扩容。slice的底层是数组指针。当append后,slice长度不超过容量cap,新增的元素将直接加在数组中。当append后,slice长度超过容量cap,将会返回一个新的slice。在进行扩容之后,将旧数据拷贝到新数组中,然后将Array指针指向新的数组地址。对于切片扩容来说。
在go的1.17之前,如果需要的容量大于扩容二倍,直接变成需要的容量;如果小于1024,容量变成原来的二倍,如果大于1024,进入一个循环,去将容量每次扩大到原来的1.25倍,直到大于需要的容量。
在go的1.18之后,如果需要的容量大于扩容二倍,直接变成需要的容量;如果小于256,容量变成原来的二倍,如果大于256,进入一个循环,去将容量每次扩大到(旧容量+3*256),直到大于需要的容量。
在go中的话,没有引用传递,只有值传递,当 channel,指针,map传递的时候,不是值的拷贝,传递的是地址。
type SliceHeader struct {
Array uintptr //指向底层数组的指针
Len int //切片的长度
Cap int //切片的容量
}
map 又称字典,是一种常用的数据结构,核心特征包含下述三点:
(1)存储基于 key-value 对映射的模式;
(2)基于 key 维度实现存储数据的去重;
(3)读、写、删操作控制,时间复杂度 O(1).
map是一种key-value键值对的存储结构,其中key是不能重复的,其底层实现采用的是hash表。
在go的map实现中,它的底层结构体是hmap,hmap里维护着若干个bucket数组 (即桶数组)。
Bucket数组中每个元素都是bmap结构,也即每个bucket(桶)都是bmap结构, 每个桶中保存了8个kv对,如果8个满了,又来了一个key落在了这个桶里,会使用overflow连接下一个桶(溢出桶)。
type hmap struct {
count int // 元素的个数
flags uint8
B uint8 // buckets 数组的长度,就是 2^B 个
noverflow uint16
hash0 uint32
buckets unsafe.Pointer // 2^B个桶对应的数组指针
oldbuckets unsafe.Pointer // 发生扩容时,记录扩容前的buckets数组指针
nevacuate uintptr
extra *mapextra //用于保存溢出桶的地址
}
//在编译期间的bmap结构体
type bmap struct {
tophash [8]uint8 //存储哈希值的高8位
data byte[1] //key value数据
overflow *bmap //溢出bucket的地址
}
当在 查 找/存放 hash的时候,首先会将key进行hash计算,将得到的hash值抽取后B位去锁定桶的索引,然后根据hash值的前八位判断在bmap中的存储位置,如果发生hash冲突的话,有两种解决办法,一种是开放寻址法,一种是拉链法。
关于hash冲突:当两个不同的 key 落在同一个桶中,就是发生了哈希冲突。冲突的解决手段是采用链表法:在 桶 中,从前往后找到第一个空位进行插入。如果8个kv满了,那么当前桶就会连接到下一个溢出桶(bmap)。
在 map 解决 hash /分桶 冲突问题时,实际上结合了拉链法和开放寻址法两种思路. 以 map 的插入写流程为例,进行思路阐述:
(1)桶数组中的每个桶,严格意义上是一个单向桶链表,以桶为节点进行串联;
(2)每个桶固定可以存放 8 个 key-value 对;
(3)当 key 命中一个桶时,首先根据开放寻址法,在桶的 8 个位置中寻找空位进行插入;
(4)倘若桶的 8 个位置都已被占满,则基于桶的溢出桶指针,找到下一个桶,重复第(3)步;
(5)倘若遍历到链表尾部,仍未找到空位,则基于拉链法,在桶链表尾部续接新桶,并插入 key-value 对.
Map的扩容机制有两种,一种是等量扩容,一种是增量扩容。
等量扩容:
由于map中不断的put和delete key,桶中可能会出现很多断断续续的空位,这些空位会导致连接的bmap溢出桶很长,导致扫描时间边长。这种扩容实际上是一种整理,把后置位的数据整理到前面。这种情况下,元素会发生重排,但不会换桶。
增量扩容:
这种2倍扩容是由于当前桶数组确实不够用了,发生这种扩容时,元素会重排,可能会发生桶迁移。
如图中所示,扩容前B=2,扩容后B=3,假设一元素key的hash值后三位为101 ,那么由上文的介绍可知,在扩容前,由hash值的后两位来决定几号桶,即 01 所以元素在1号桶。 在扩容发生后,由hash值得后三位来决定几号桶,即101所以元素会迁移到5号桶。
扩容的条件有两种:
一种是装载因子(map中键值对数量/map中桶的个数2^B)>6.5 即每个桶中元素超过6.5个时候,意味着当前容量要不足了,发生扩容 增量扩容
第二种是 当溢出桶太多,在桶数量小于2^15次方的时候, 当溢出桶数量大于等于桶的数量,认为溢出桶过多,当桶总数大于等于2^15次方的时候,溢出桶数量大于等于 2^15次方时候,就认为溢出桶数量过多。
map 不是并发安全的数据结构,倘若存在并发读写行为,会抛出 fatal error.,并发读写会引发 fatal error,是一种比 panic 更严重的错误,无法使用 recover 操作捕获.
CSP模型
Golang 就是借用CSP模型的一些概念为之实现并发进行理论支持,其实从实际上出发,go语言并没有,完全实现了CSP模型的所有理论,仅仅是借用了 process和channel这两个概念。
每个实体之间是通过channel通讯来实现数据共享。
channel是golang中用来实现多个goroutine通信的管道,它的底层是一个叫做hchan的结构体。
type hchan struct {
//channel分为无缓冲和有缓冲两种。
//对于有缓冲的channel存储数据,借助的是如下循环数组的结构
qcount uint // 循环数组中的元素数量
dataqsiz uint // 循环数组的长度
buf unsafe.Pointer // 指向底层循环数组的指针
elemsize uint16 //能够收发元素的大小
closed uint32 //channel是否关闭的标志
elemtype *_type //channel中的元素类型
//有缓冲channel内的缓冲数组会被作为一个“环型”来使用。
//当下标超过数组容量后会回到第一个位置,所以需要有两个字段记录当前读和写的下标位置
sendx uint // 下一次发送数据的下标位置
recvx uint // 下一次读取数据的下标位置
//当循环数组中没有数据时,收到了接收请求,那么接收数据的变量地址将会写入读等待队列
//当循环数组中数据已满时,收到了发送请求,那么发送数据的变量地址将写入写等待队列
recvq waitq // 读等待队列
sendq waitq // 写等待队列
lock mutex //互斥锁,保证读写channel时不存在并发竞争问题
}
hchan结构体的主要组成部分有四个:
缓存数据用的循环链表。=> buf。
需要发送数据的index / 需要接受数据的index => sendx和recvx。
接收的协程队列 / 发送的协程队列 => recvq / sendq
互斥锁 => lock
sendq 和 recvq 存储了当前 Channel 由于缓冲区空间不足而阻塞的 Goroutine 列表,这些等待队列使用双向链表 runtime.waitq 表示,链表中所有的元素都是 runtime.sudog 结构:
type waitq struct {
first *sudog
last *sudog
}
type sudog struct {
g *g
next *sudog
prev *sudog
elem unsafe.Pointer
c *hchan
...
}
runtime.sudog 表示一个在等待列表中的 Goroutine,该结构中存储了两个分别指向前后 runtime.sudog 的指针以构成链表。
双向链表,包含一个头结点和一个尾结点
每个节点是一个sudog结构体变量,记录哪个协程在等待,等待的是哪个channel,等待发送/接收的数据在哪里
创建策略
如果是无缓冲的 channel,会直接给 hchan 分配内存
如果是有缓冲的 channel,并且元素不包含指针,那么会为 hchan 和底层数组分配一段连续的地址
如果是有缓冲的 channel,并且元素包含指针,那么会为 hchan 和底层数组分别分配地址
向channel中发送数据
如果 channel 的读等待队列存在接收者goroutine
将数据直接发送给第一个等待的 goroutine, 唤醒接收的 goroutine
如果 channel 的读等待队列不存在接收者goroutine
如果循环数组buf未满,那么将会把数据发送到循环数组buf的队尾
如果循环数组buf已满,这个时候就会走阻塞发送的流程,将当前 goroutine 加入写等待队列,并挂起等待唤醒
向channel中接收数据
如果 channel 的写等待队列存在发送者goroutine
如果是无缓冲 channel,直接从第一个发送者goroutine那里把数据拷贝给接收变量,唤醒发送的 goroutine
如果是有缓冲 channel(已满),将循环数组buf的队首元素拷贝给接收变量,将第一个发送者goroutine的数据拷贝到 buf循环数组队尾,唤醒发送的 goroutine
当在向通道进行发送数据时候,会先对buf进行上锁操作,然后将数据copy到buf中,然后进行sendx++,然后释放对buf的锁
当在向通道进行接收数据时候,会先对buf进行上锁操作,然后将buf中的数据copy赋值变量中,然后recvx++,并释放锁
在Go中对于并发程序进行公共资源的访问的限制最常用的就是互斥锁(sync.mutex)的方式
type Mutex struct {
state int32
sema uint32
}
sync.mutex的常用方法有两个:
Mutex.lock()用来获取锁
Mutex.Unlock()用于释放锁
数据库三大范式 是指在关系型数据库设计中,为了避免数据不一致和数据不一致性而需要遵循的三个规范化级别。作用减少数据和数据不一致性,提高数据库的数据完整性和可靠性。
第一范式(1NF)无重复的列(原子性)是指数据库表的每一列都是不可分割的基本数据项。所有的域都应该是原子性的,即数据库表的每一列都是不可分割的原子数据项,而不能是集合,数组,记录等非原子数据项。即实体中的某个属性有多个值时,必须拆分为不同的属性。
第二范式(2NF)非主键属性完全依赖于主键,就是非主属性完全依赖于主关键字
第三范式(3NF)属性不依赖于其它非主属性,保证非主键属性之间不存在供给依赖关系
关系型数据库最典型的数据结构是表,由二维表及其之间的联系所组成的一个数据组织。
优点: 1、易于维护:都是使用表结构,格式一致; 2、使用方便:SQL语言通用,可用于复杂查询;
3、复杂操作:支持SQL,可用于一个表以及多个表之间非常复杂的查询。
缺点: 1、读写性能比较差,尤其是海量数据的高效率读写;
2、固定的表结构,灵活度稍欠; 3、高并发读写需求,传统关系型数据库来说,硬盘I/O是一个很大的瓶颈。
场景:
使用场景:
1)需要做复杂处理的数据;
2)数据量不是特别大的数据;
3)对安全性要求高的数据;
4)数据格式单一的数据;
非关系型数据库严格上不是一种数据库,应该是一种数据结构化存储方法的集合,可以是文档或者键值对等。
优点: 1、格式灵活:存储数据的格式可以是key,value形式、文档形式、图片形式等等,文档形式、图片形式等等,使用灵活,应用场景广泛,而关系型数据库则只支持基础类型。
2、速度快:nosql可以使用硬盘或者随机存储器作为载体,而关系型数据库只能使用硬盘; 3、高扩展性;
4、成本低:nosql数据库部署简单,基本都是开源软件。
缺点: 1、不提供sql支持,学习和使用成本较高; 2、无事务处理;
3、数据结构相对复杂,复杂查询方面稍欠。
使用场景:
1)海量数据存储;
2)多格式的数据存储;
3)对查询速度要求快的数据存储;
MySQL(关系型数据库)、MongoDB 和 Redis 是常见的 NoSQL 数据库
MySQL
属于关系型数据库,可以存储结构化数据,支持跨表联合查询等复杂操作,尤其适用于事务处理等需要严格保障数据安全的场景。在实习项目中,MySQL 主要用于存储主要业务数据,如用户信息、订单信息、产品信息等。具体包括:
存储结构化数据;
支持 SQL 语句,通过各种 SQL 聚合函数可以进行灵活的数据统计和分析;
支持跨表联合查询等复杂操作;
支持事务和 MVCC 特性。
MongoDB
是一个开源文档型数据库,也称非关系型数据库(NoSQL),能够轻松处理半结构化或非结构化的数据。在实习项目中,MongoDB
可能用于存储日志数据、浏览器行为数据等。它具有以下特点: 面向文档设计,易于存储和检索半结构化和非结构化数据;
插入记录比传统关系型数据库更快; 可充当键值对存储,可以快速存储小型记录,类似 Redis;
支持数据分片、副本集和自动故障转移等灵活的高可靠性方案。
Redis
是一种开源的内存数据库,也被称为缓存和键值对存储。在实习项目中,Redis
用于缓存热门商品、用户信息等。它具有以下特点: 内存数据库,读写速度极快; 主要用作缓存系统或者分布式锁; 支持文本、二进制数据等多种格式;
拥有数据过期机制等高级特性; 支持多种数据结构,如字符串、哈希表、列表、集合和有序集合等。 形成体系化应用
综上所述,三种主流的数据库各有其适用场景和特点。在实际使用中,我们可以根据具体的业务需求,将它们结合起来形成一个完整的体系化应用。比如,通过
MySQL 等关系型数据库管理用户账户、权限等基础数据,利用 MongoDB 存储大量的半结构化数据,再通过 Redis 等缓存数据库加速访问速度,从而提高整个系统的性能和稳定性。
从字段、事务、索引、避免全表查询等角度展开
字段优化:
选择合适的数据类型:选择最合适的数据类型来存储数据,以减小存储空间的开销。不要使用过大的数据类型,例如使用 INT 而不是
BIGINT,除非有必要。避免使用过多的 NULL 值:NULL 值会占用额外的存储空间,尽量避免过多的 NULL 值。
使用整数作为主键:整数类型的主键比字符串类型的主键更加高效,因为整数的比较速度更快。
垂直分割表:将大型表拆分成多个小型表,只包含必要的字段,以减少不必要的数据读取。
事务优化:
尽量减小事务的范围:将事务的范围缩小到最小,只涵盖必要的操作,以减少锁的竞争和事务的冲突。
使用批量操作:使用批量插入、更新或删除操作,而不是逐条操作,以减少事务的开销。
合理使用事务隔离级别:根据应用的需求选择合适的事务隔离级别,避免过高的隔离级别导致性能下降。
索引优化:
选择适当的索引:根据查询的需求选择合适的索引,不要过度索引表。复合索引可以减少索引的数量。
定期重建索引:定期对表的索引进行重建或优化,以保持索引的性能。
使用覆盖索引:尽量使用覆盖索引,这样可以减少数据库访问磁盘的次数,提高查询性能。
避免全表查询:
使用合适的 WHERE 子句:确保每个查询都包含合适的 WHERE 子句,以减少不必要的数据检索。
使用分页:对于大型数据集,使用分页来限制每次查询返回的数据量,避免一次性查询整个表。
缓存查询结果:对于频繁执行相同查询的情况,可以考虑使用缓存来存储查询结果,减少数据库访问。
使用索引覆盖:已经在索引中包含了查询所需的所有字段时,可以避免全表扫描,提高查询速度。
6)JOIN和UNION的区别
首先对JOIN进行讲解:
A)内连接:join,inner join
B)外连接:left join,left outerjoin,right join,right outer join,union
C)交叉连接:cross join
内连接:
应用场景:
左连接:
应用场景:
右连接:
应用场景:
join(连接查询) 与 union(联合查询)
7)如何进行多表查询
8)为什么要分库分表,如何进行分库分表
9)left union、inner union的区别
10)主从复制的原理
为何需要主从复制,原理是什么,如何去实现主从复制
11)一条SQL语句的执行过程,SELECT是如何执行的,UPDATE是如何执行的
连接器—缓存—分析器—优化器—执行器
12)什么是索引,为什么要用索引,索引的优缺点
13)索引的实现方式/索引采用的数据结构,它们之间的区别
B+树,Hash,B树
B+树和B树的区别
B+树和Hash的区别
B+树索引的缺点
14)索引的类型、索引的种类
FULLTEXT,BTREE,HASH等
主键索引,组合索引,唯一索引等
15)聚簇索引和非聚簇索引
16)什么是回表查询,非聚簇索引一定要回表查询吗
17)索引的应用场景
18)索引什么时候会失效
19)唯一索引和主键索引的区别
20)给出一条SQL,判断是否走联合索引
21)最左前缀匹配规则
22)Mysql中sql语句执行太慢,是什么原因,怎么解决,用什么命令查看
23)如何查看是否应用索引
24)加了索引为什么会变快
25)MySQL数据库有哪些锁
26)共享锁、更新锁、排他锁
27)什么是死锁,如何解决死锁,如何避免死锁?
28)数据库的行锁和表锁
29)什么是脏读、不可重复读、幻读
30)事务的隔离级别
31)事务的四大特性(ACID),MySQL是如何实现ACID的
32)MySQL的存储引擎
33)MVCC
SOA (面向服务)是什么?
将服务拆分比较小,bug少,容易测试和维护,也容易扩展。
单一职责,一个服务只干一件事。
微服务是SOA的一种实践,围绕业务功能构建的,服务关注单一业务,服务间采用轻量级的通信机制,可以全自动独立部署,可以使用不同的编程语言和数据存储技术。
优点:
解耦——系统中的服务在很大程度上是解耦的。因此,整个应用程序可以很容易地构建、修改和伸缩
组件化——微服务被视为独立的组件,可以很容易地替换和升级 业务功能——微服务非常简单,只关注一个功能
自治——开发人员和团队可以彼此独立工作,从而提高速度 持续交付——通过软件创建、测试和批准的系统自动化,允许频繁地发布软件
责任——微服务不关注应用程序作为项目。相反,他们将应用程序视为自己负责的产品
分散治理——重点是为正确的工作使用正确的工具。这意味着没有标准化的模式或任何技术模式。开发人员可以自由选择最有用的工具来解决他们的问题
敏捷——微服务支持敏捷开发。任何新特性都可以快速开发并再次丢弃
微服务架构和单体架构是两种不同的软件架构模式,微服务架构通常更适用于大型、复杂的应用程序,而单体架构可能更适合小型项目或快速原型开发。它们在设计和组织应用程序方面有很大的区别:
Go面试题(三):map的实现原理
微信公众号 小徐先生的编程世界
Go面试题(五):图解 Golang Channel 的底层原理
Go底层原理 - channel的底层实现原理?
Go CSP模型 介绍
深入详解Go的channel底层实现原理【图解】