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


    写在前面

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

    笔试

    无笔试,很奇怪(

    一面

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

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

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

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

    进程之间如何进行通信?

    管道、消息队列、共享内存、信号量、信号、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: 是一个指针,指向溢出桶,当该桶不够用时,就会使用溢出桶
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    在这里插入图片描述
    当向 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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    通过对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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    怎么解决?

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

    什么是事务?

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

    那 redis 支持事务吗?

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

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

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

    二面

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

    真的是 海量 hc 吗。。。

  • 相关阅读:
    「Verilog学习笔记」实现3-8译码器①
    STL库:list
    关于JAVA中字节码文件版本号、产品版本号及开发版本号的关系
    (续)SSM整合之spring笔记(IOC 依赖注入之为属性赋值)(P073—P077)
    【Codeforces】Educational Codeforces Round 156 [Rated for Div. 2]
    LinkedList源码分析
    STC8/15单片机EEPROM外部加载和内部读写
    k8s2-5日常使用操作指令
    六大科研工具推荐,外文文献阅读管理全都搞定!
    5分钟带你了解什么是敏捷测试?难点显而易见!
  • 原文地址:https://blog.csdn.net/weixin_45304503/article/details/126689946