【Golang开发面经】米哈游(一轮游)

文章目录

  • 写在前面
  • 笔试
  • 一面
    • 线程和协程有什么区别?各自有什么优缺点?
    • 进程之间如何进行通信?
    • 什么是信号,信号量是如何实现的?
    • 讲讲Go里面的GMP模型?
    • Go的GMP模型
    • map用过吧?怎么对map进行排序?
    • 讲讲map底层?为什么是无序的?
    • 知道TCP连接吧?三次握手的目的是什么?
    • 那四次挥手有了解过吗?为什么是四次?
    • 什么是粘包和拆包?为什么会出现?
    • 怎么解决?
    • 什么是事务?
    • 那 redis 支持事务吗?
    • 算法:手撕链表,要求递归实现。
    • 算法:忘记了。。好像是中等题,反正不难,也不简单。
  • 二面

写在前面

米哈游 面试下来感觉还行吧,挺注重基础的,面试官水平也很高。但是感觉不是在招人的样子,我有好多同学都是简历挂(

笔试

无笔试,很奇怪(

一面

线程和协程有什么区别?各自有什么优缺点?

进程 线程
系统中正在运行的一个应用程序 系统分配处理器时间资源的基本单元
程序一旦运行就是进程 进程之内独立执行的一个单元执行流
资源分配的最小单位 程序执行的最小单位

进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。

线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些 要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程

进程之间如何进行通信?

管道、消息队列、共享内存、信号量、信号、socket

什么是信号,信号量是如何实现的?

信号:
在Linux中,为了响应各种事件,提供了几十种信号,可以通过kill -l命令查看。

如果是运行在 shell终端 的进程,可以通过键盘组合键来给进程发送信号,例如使用Ctrl+C 产生 SIGINT 信号,表示终止进程。

如果是运行在后台的进程,可以通过命令来给进程发送信号,例如使用kill -9 PID 产生SIGKILL信号,表示立即结束进程。

信号量:

当使用共享内存的通信方式,如果有多个进程同时往共享内存写入数据,有可能先写的进程的内容被其他进程覆盖了。

因此需要一种保护机制,信号量本质上是一个整型的计数器,用于实现进程间的互斥和同步

信号量代表着资源的数量,操作信号量的方式有两种:

  • P操作:这个操作会将信号量减一,相减后信号量如果小于0,则表示资源已经被占用了,进程需要阻塞等待;如果大于等于0,则说明还有资源可用,进程可以正常执行。
  • V操作:这个操作会将信号量加一,相加后信号量如果小于等于0,则表明当前有进程阻塞,于是会将该进程唤醒;如果大于0,则表示当前没有阻塞的进程。

讲讲Go里面的GMP模型?

Go的GMP模型

G:表示goroutine,存储了goroutine的执行stack信息、goroutine状态以及goroutine的任务函数等;另外G对象是可以重用的。
P:表示逻辑processor,P 的数量决定了系统内最大可并行的 G 的数量(前提:系统的物理cpu核数 >= P的数量);P的最大作用还是其拥有的各种G对象队列、链表、一些cache和状态。
M:M 代表着真正的执行计算资源,物理 Processor。

G 如果想运行起来必须依赖 P,因为 P 是它的逻辑处理单元,但是 P 要想真正的运行,他也需要与 M 绑定,这样才能真正的运行起来,P 和 M 的这种关系就相当于 Linux 系统中的用户层面的线程和内核的线程是一样的

map用过吧?怎么对map进行排序?

go的map不保证有序性,所以按key排序需要取出key,对key排序,再遍历输出value

讲讲map底层?为什么是无序的?

map 的底层是一个结构体

// Go map 的底层结构体表示
type hmap struct {
    count     int    // map中键值对的个数,使用len()可以获取 
	flags     uint8
	B         uint8  // 哈希桶的数量的log2,比如有8个桶,那么B=3
	noverflow uint16 // 溢出桶的数量
	hash0     uint32 // 哈希种子

	buckets    unsafe.Pointer // 指向哈希桶数组的指针,数量为 2^B 
	oldbuckets unsafe.Pointer // 扩容时指向旧桶的指针,当扩容时不为nil 
	nevacuate  uintptr        

	extra *mapextra  // 可选字段
}

const (
	bucketCntBits = 3
	bucketCnt     = 1 << bucketCntBits     // 桶数量 1 << 3 = 8
)

// Go map 的一个哈希桶,一个桶最多存放8个键值对
type bmap struct {
    // tophash存放了哈希值的最高字节
	tophash [bucketCnt]uint8
    
    // 在这里有几个其它的字段没有显示出来,因为k-v的数量类型是不确定的,编译的时候才会确定
    // keys: 是一个数组,大小为bucketCnt=8,存放Key
    // elems: 是一个数组,大小为bucketCnt=8,存放Value
    // 你可能会想到为什么不用空接口,空接口可以保存任意类型。但是空接口底层也是个结构体,中间隔了一层。因此在这里没有使用空接口。
    // 注意:之所以将所有key存放在一个数组,将value存放在一个数组,而不是键值对的形式,是为了消除例如map[int64]所需的填充整数8(内存对齐)
    
    // overflow: 是一个指针,指向溢出桶,当该桶不够用时,就会使用溢出桶
}


当向 map 中存储一个 kv 时,通过 k 的 hash 值与 buckets 长度取余,定位到 key 在哪一个bucket中,hash 值的高8位存储在 bucket 的 tophash[i] 中,用来快速判断 key是否存在。当一个 bucket 满时,通过 overflow 指针链接到下一个 bucket。

在源码runtime.mapiterinit

func mapiterinit(t *maptype, h *hmap, it *hiter) {
	...
	it.t = t
	it.h = h
	it.B = h.B
	it.buckets = h.buckets
	if t.bucket.kind&kindNoPointers != 0 {
		h.createOverflow()
		it.overflow = h.extra.overflow
		it.oldoverflow = h.extra.oldoverflow
	}

	r := uintptr(fastrand())
	if h.B > 31-bucketCntBits {
		r += uintptr(fastrand()) << 31
	}
	it.startBucket = r & bucketMask(h.B)
	it.offset = uint8(r >> h.B & (bucketCnt - 1))
	it.bucket = it.startBucket
    ...

	mapiternext(it)
}

通过对 mapiterinit 方法阅读,可得知其主要用途是在 map 进行遍历迭代时进行初始化动作。共有三个形参,用于读取当前哈希表的类型信息、当前哈希表的存储信息和当前遍历迭代的数据

源码中 fastrand

...
// decide where to start
r := uintptr(fastrand())
if h.B > 31-bucketCntBits {
	r += uintptr(fastrand()) << 31
}
it.startBucket = r & bucketMask(h.B)
it.offset = uint8(r >> h.B & (bucketCnt - 1))

// iterator state
it.bucket = it.startBucket

在这段代码中,它生成了随机数。用于决定从哪里开始循环迭代。更具体的话就是根据随机数,选择一个桶位置作为起始点进行遍历迭代。

因此每次重新 for range map,你见到的结果都是不一样的。那是因为它的起始位置根本就不固定!

知道TCP连接吧?三次握手的目的是什么?

因为通信的前提是确保双方都是接收和发送信息是正常的。
三次握手是为了建立可靠的数据传输通道
第一次握手就是让 接收方 知道 发送方 有发送信息的能力
第二次握手就是让 发送方 知道 接收方 有发送和接受信息的能力
第三次握手就是让 接收方 知道 发送方 有接受信息的能力

那四次挥手有了解过吗?为什么是四次?

四次挥手则是为了保证等数据完成的被接收完再关闭连接。

既然提到需要保证数据完整的传输完,那就需要保证双方 都达到关闭连接的条件才能断开。

第一次挥手:客户端发起关闭连接的请求给服务端;

第二次挥手:服务端收到关闭请求的时候可能这个时候数据还没发送完,所以服务端会先回复一个确认报文,表示自己知道客户端想要关闭连接了,但是因为数据还没传输完,所以还需要等待;

第三次挥手:当数据传输完了,服务端会主动发送一个 FIN 报文,告诉客户端,表示数据已经发送完了,服务端这边准备关闭连接了

第四次挥手:当客户端收到服务端的 FIN 报文过后,会回复一个 ACK 报文,告诉服务端自己知道了,再等待一会就关闭连接。

什么是粘包和拆包?为什么会出现?

粘包就是两个或者多个以上的包粘在一起,拆包就是为什么解决粘包问题。

出现粘包原因有两个,一个是发送方发送不及时,导致多个包在发送端粘黏。另一个是接收方接受不及时,导致包在接收端堆叠导致。

怎么解决?

设计一个带包头的应用层报文结构就能解决。包头定长,以特定标志开头,里带着负载长度,这样接收侧只要以定长尝试读取包头,再按照包头里的负载长度读取负载就行了,多出来的数据都留在缓冲区里即可。其实ip报文和tcp报文都是这么干的。

什么是事务?

事务具有原子性、一致性、隔离性、持久性特性,并且 指将一系列数据操作捆绑成为一个整体进行统一管理,如果某一事务执行成功,则在该事物中进行的所有数据更改均会提交,成为数据库中的永久组成部分。

那 redis 支持事务吗?

redis 是不支持事务的,因为 他不支持原子性,但是支持一致性,是最终一致性。

算法:手撕链表,要求递归实现。

算法:忘记了。。好像是中等题,反正不难,也不简单。

二面

我以为过了简历,免了笔试,一面算法写出来,八股也能吹了,应该有二面,但还是挂了(

真的是 海量 hc 吗。。。

你可能感兴趣的:(Go语言面试题目,golang,linux,服务器)