前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【建议收藏】吐血整理Golang面试干货21问-吊打面试官-1

【建议收藏】吐血整理Golang面试干货21问-吊打面试官-1

作者头像
公众号-利志分享
发布2022-04-25 09:43:09
1.4K0
发布2022-04-25 09:43:09
举报
文章被收录于专栏:利志分享利志分享

Golang面试分享来了,为了帮助大家更好的面试,笔者总结一份相关的Golang知识的面试问题,希望能帮助大家。

问:Go函数参数传递方式?

Go的函数参数传递都是值传递。所谓值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。参数传递还有引用传递,所谓引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

问:Go函数参数传递是值传递,为什么map,slice,chan可能在函数内被修改?

答:因为Go里面的map,slice,chan是引用类型。变量区分值类型和引用类型。所谓值类型:变量和变量的值存在同一个位置。所谓引用类型:变量和变量的值是不同的位置,变量的值存储的是对值的引用。但并不是map,slice,chan的所有的变量在函数内都能被修改,不同数据类型的底层存储结构和实现可能不太一样,情况也就不一样。

问:讲讲Go的slice底层数据结构和一些特性?

答:Go的slice底层数据结构是由一个array指针指向底层数组,len表示切片长度,cap表示切片容量。slice的主要实现是扩容。对于append向slice添加元素时,假如slice容量够用,则追加新元素进去,slice.len++,返回原来的slice。当原容量不够,则slice先扩容,扩容之后slice得到新的slice,将元素追加进新的slice,slice.len++,返回新的slice。对于切片的扩容规则:当切片比较小时(容量小于1024),则采用较大的扩容倍速进行扩容(新的扩容会是原来的2倍),避免频繁扩容,从而减少内存分配的次数和数据拷贝的代价。当切片较大的时(原来的slice的容量大于或者等于1024),采用较小的扩容倍速(新的扩容将扩大大于或者等于原来1.25倍),主要避免空间浪费,网上其实很多总结的是1.25倍,那是在不考虑内存对齐的情况下,实际上还要考虑内存对齐,扩容是大于或者等于1.25倍。(关于刚才问的slice为什么传到函数内可能被修改,如果slice在函数内没有出现扩容,函数外和函数内slice变量指向是同一个数组,则函数内复制的slice变量值出现更改,函数外这个slice变量值也会被修改。如果slice在函数内出现扩容,则函数内变量的值会新生成一个数组(也就是新的slice,而函数外的slice指向的还是原来的slice,则函数内的修改不会影响函数外的slice。)

问:讲讲Go的map底层数据结构和一些特性,map是否是线程安全?

答:golang中map是一个kv对集合。底层使用hash table,用链表来解决冲突 ,出现冲突时,不是每一个key都申请一个结构通过链表串起来,而是以bmap为最小粒度挂载,一个bmap可以放8个kv。在哈希函数的选择上,会在程序启动时,检测 cpu 是否支持 aes,如果支持,则使用 aes hash,否则使用 memhash。每个map的底层结构是hmap,是有若干个结构为bmap的bucket组成的数组。每个bucket底层都采用链表结构。hmap的结构如下:

代码语言:javascript
复制
type hmap struct {
    count     int                  // 元素个数
    flags     uint8
    B         uint8                // 扩容常量相关字段B是buckets数组的长度的对数 2^B
    noverflow uint16               // 溢出的bucket个数
    hash0     uint32               // hash seed
    buckets    unsafe.Pointer      // buckets 数组指针
    oldbuckets unsafe.Pointer      // 结构扩容的时候用于赋值的buckets数组
    nevacuate  uintptr             // 搬迁进度
    extra *mapextra                // 用于扩容的指针
}

map的容量大小 底层调用makemap函数,计算得到合适的B,map容量最多可容纳6.52^B个元素,6.5为装载因子阈值常量。装载因子的计算公式是:装载因子=填入表中的元素个数/散列表的长度,装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降。底层调用makemap函数,计算得到合适的B,map容量最多可容纳6.52^B个元素,6.5为装载因子阈值常量。装载因子的计算公式是:装载因子=填入表中的元素个数/散列表的长度,装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降。 触发 map 扩容的条件 1)装载因子超过阈值,源码里定义的阈值是 6.5。 2)overflow 的 bucket 数量过多 map的bucket定位和key的定位 高八位用于定位 bucket,低八位用于定位 key,快速试错后再进行完整对比

问:讲讲Go的chan底层数据结构和主要使用场景

答:channel的数据结构包含qccount当前队列中剩余元素个数,dataqsiz环形队列长度,即可以存放的元素个数,buf环形队列指针,elemsize每个元素的大小,closed标识关闭状态,elemtype元素类型,sendx队列下表,指示元素写入时存放到队列中的位置,recv队列下表,指示元素从队列的该位置读出。recvq等待读消息的goroutine队列,sendq等待写消息的goroutine队列,lock互斥锁,chan不允许并发读写。 无缓冲和有缓冲区别: 管道没有缓冲区,从管道读数据会阻塞,直到有协程向管道中写入数据。同样,向管道写入数据也会阻塞,直到有协程从管道读取数据。管道有缓冲区但缓冲区没有数据,从管道读取数据也会阻塞,直到协程写入数据,如果管道满了,写数据也会阻塞,直到协程从缓冲区读取数据。 channel的一些特点

  1. 读写值nil管道会永久阻塞
  2. 关闭的管道读数据仍然可以读数据
  3. 往关闭的管道写数据会panic
  4. 关闭为nil的管道panic
  5. 关闭已经关闭的管道panic

向channel写数据的流程:

  1. 如果等待接收队列recvq不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从recvq取出G,并把数据写入,最后把该G唤醒,结束发送过程;
  2. 如果缓冲区中有空余位置,将数据写入缓冲区,结束发送过程;
  3. 如果缓冲区中没有空余位置,将待发送数据写入G,将当前G加入sendq,进入睡眠,等待被读goroutine唤醒;

向channel读数据的流程:

  1. 如果等待发送队列sendq不为空,且没有缓冲区,直接从sendq中取出G,把G中数据读出,最后把G唤醒,结束读取过程;
  2. 如果等待发送队列sendq不为空,此时说明缓冲区已满,从缓冲区中首部读出数据,把G中数据写入缓冲区尾部,把G唤醒,结束读取过程;
  3. 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程;将当前goroutine加入recvq,进入睡眠,等待被写goroutine唤醒;

使用场景: 消息传递、消息过滤,信号广播,事件订阅与广播,请求、响应转发,任务分发,结果汇总,并发控制,限流,同步与异步

问:讲讲Go的select底层数据结构和一些特性?

答:简介:go的select为golang提供了多路IO复用机制,和其他IO复用一样,用于检测是否有读写事件是否ready。linux的系统IO模型有select,poll,epoll,go的select和linux系统select非常相似。 数据结构如下: select结构组成主要是由case语句和执行的函数组成 select实现的多路复用是:每个线程或者进程都先到注册和接受的channel(装置)注册,然后阻塞,然后只有一个线程在运输,当注册的线程和进程准备好数据后,装置会根据注册的信息得到相应的数据。 select的特性

  1. select操作至少要有一个case语句,出现读写nil的channel该分支会忽略,在其操作则会报错。
  2. select仅支持管道,而且是单协程操作。
  3. 每个case语句仅能处理一个管道,要么读要么写。
  4. 多个case语句的执行顺序是随机的。
  5. 存在default语句,select将不会阻塞,但是存在default会影响性能。
问:讲讲Go的defer底层数据结构和一些特性?

答:每个defer语句都对应一个_defer实例,多个实例使用指针连接起来形成一个单连表,保存在gotoutine数据结构中,每次插入_defer实例,均插入到链表的头部,函数结束再一次从头部取出,从而形成后进先出的效果。defer的规则总结:

  1. 延迟函数的参数是defer语句出现的时候就已经确定了的。
  2. 延迟函数执行按照后进先出的顺序执行,即先出现的defer最后执行。
  3. 延迟函数可能操作主函数的返回值。
  4. 申请资源后立即使用defer关闭资源是个好习惯。
问:Go 中解析 tag 是怎么实现的?反射原理是什么?

答:Go中解析的tag是通过反射实现的,反射是指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力或动态知道给定数据对象的类型和结构,并有机会修改它。反射将接口变量转换成反射对象 Type 和 Value;反射可以通过反射对象 Value 还原成原先的接口变量;反射可以用来修改一个变量的值,前提是这个值可以被修改。

问:讲讲Go的GPM模型

答:G代表着goroutine,P代表着上下文处理器,M代表thread线程,在GPM模型,有一个全局队列(Global Queue):存放等待运行的G,还有一个P的本地队列:也是存放等待运行的G,但数量有限,不超过256个。GPM的调度流程从go func()开始创建一个goroutine,新建的goroutine优先保存在P的本地队列中,如果P的本地队列已经满了,则会保存到全局队列中。M会从P的队列中取一个可执行状态的G来执行,如果P的本地队列为空,就会从其他的MP组合偷取一个可执行的G来执行,当M执行某一个G时候发生系统调用或者阻塞,M阻塞,如果这个时候G在执行,runtime会把这个线程M从P中摘除,然后创建一个新的操作系统线程来服务于这个P,当M系统调用结束时,这个G会尝试获取一个空闲的P来执行,并放入到这个P的本地队列,如果这个线程M变成休眠状态,加入到空闲线程中,然后整个G就会被放入到全局队列中。关于G,P,M的个数问题,G的个数理论上是无限制的,但是受内存限制,P的数量一般建议是逻辑CPU数量的2倍,M的数据默认启动的时候是10000,内核很难支持这么多线程数,所以整个限制客户忽略,M一般不做设置,设置好P,M一般都是要大于P。

问:讲讲Go的GC回收机制

答:Go的GC回收有三次演进过程,Go V1.3之前普通标记清除(mark and sweep)方法,整体过程需要启动STW,效率极低。GoV1.5三色标记法,堆空间启动写屏障,栈空间不启动,全部扫描之后,需要重新扫描一次栈(需要STW),效率普通。GoV1.8三色标记法,混合写屏障机制:栈空间不启动(全部标记成黑色),堆空间启用写屏障,整个过程不要STW,效率高。

  1. Go1.3之前的版本所谓标记清除是先启动STW暂停,然后执行标记,再执行数据回收,最后停止STW。Go1.3版本标记清除做了点优化,流程是:先启动STW暂停,然后执行标记,停止STW,最后再执行数据回收。
  2. Go1.5三色标记主要是插入屏障和删除屏障,写入屏障的流程:程序开始,全部标记为白色,1)所有的对象放到白色集合,2)遍历一次根节点,得到灰色节点,3)遍历灰色节点,将可达的对象,从白色标记灰色,遍历之后的灰色标记成黑色,4)由于并发特性,此刻外界向在堆中的对象发生添加对象,以及在栈中的对象添加对象,在堆中的对象会触发插入屏障机制,栈中的对象不触发,5)由于堆中对象插入屏障,则会把堆中黑色对象添加的白色对象改成灰色,栈中的黑色对象添加的白色对象依然是白色,6)循环第5步,直到没有灰色节点,7)在准备回收白色前,重新遍历扫描一次栈空间,加上STW暂停保护栈,防止外界干扰(有新的白色会被添加成黑色)在STW中,将栈中的对象一次三色标记,直到没有灰色,8)停止STW,清除白色。至于删除写屏障,则是遍历灰色节点的时候出现可达的节点被删除,这个时候触发删除写屏障,这个可达的被删除的节点也是灰色,等循环三色标记之后,直到没有灰色节点,然后清理白色,删除写屏障会造成一个对象即使被删除了最后一个指向它的指针也依旧可以活过这一轮,在下一轮GC中被清理掉。
  3. GoV1.8混合写屏障规则是:1)GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW),2)GC期间,任何在栈上创建的新对象,均为黑色。3)被删除的对象标记为灰色。4)被添加的对象标记为灰色。
问:讲讲Go中主协程如何等待其余协程退出

答:Go的sync.WaitGroup是等待一组协程结束,sync.WaitGroup只有3个方法,Add()是添加计数,Done()减去一个计数,Wait()阻塞直到所有的任务完成。Go里面还能通过有缓冲的channel实现其阻塞等待一组协程结束,这个不能保证一组goroutine按照顺序执行,可以并发执行协程。Go里面能通过无缓冲的channel实现其阻塞等待一组协程结束,这个能保证一组goroutine按照顺序执行,但是不能并发执行。

问:Go中的make和new的区别?

答:make和new都是golang用来分配内存的內建函数,且在堆上分配内存,make 即分配内存,也初始化内存。new只是将内存清零,并没有初始化内存。make是用于引用类型(map,chan,slice)的创建,返回引用类型的本身,new创建的是指针类型,new可以分配任意类型的数据,返回的是指针。

问:Go中context 结构是什么样的?使用场景和用途

答:Go的Context的数据结构包含Deadline,Done,Err,Value,Deadline方法返回一个time.Time,表示当前Context应该结束的时间,ok则表示有结束时间,Done方法当Context被取消或者超时时候返回的一个close的channel,告诉给context相关的函数要停止当前工作然后返回了,Err表示context被取消的原因,Value方法表示context实现共享数据存储的地方,是协程安全的。context在业务中是经常被使用的,其主要的应用1:上下文控制,2:多个goroutine之间的数据交互等,3:超时控制:到某个时间点超时,过多久超时。

问:Go中 rune 类型

答:rune类型实质其实就是int32,在处理字符串及其便捷的字符单位。它会自动按照字符独立的单位去处理方便我们在遍历过程中按照我们想要的方式去遍历。

问:Go如何捕获异常

答:Go的异常处理主要通过defer func(){}()实现闭包,函数内if err :=revover();err!=nil{}来实现,if里面打印异常,关闭资源,或者退出此函数等。完整代码如下:

代码语言:javascript
复制
defer func() {
        if err := recover(); err != nil {
            // 打印异常,关闭资源,退出此函数
            fmt.Println(err)
        }
    }()
问:Go语言中不同的类型如何比较是否相等?

答:像string,int,float interface等可以通过reflect.DeepEqual和等于号进行比较,像slice,struct,map则一般使用reflect.DeepEqual来检测是否相等。

问:Go语言uint32最大值加1会怎么样?

答:溢出,报错

问:协程,线程,进程的区别?

答:

  1. 进程:是应用程序的启动实例,每个进程都有独立的内存空间,不同的进程通过进程间的通信方式来通信。
  2. 线程:从属于进程,每个进程至少包含一个线程,线程是CPU调度的基本单位,多个线程之间可以共享进程的资源并通过共享内存等线程间的通信方式来通信。
  3. 协程:为轻量级线程,与线程相比,协程不受操作系统的调度,协程的调度器由用户应用程序提供,协程调度器按照调度策略把协程调度到线程中运行。
问:Go中init函数的特征

答:一个包下可以有多个init函数,每个文件也可以有多个init 函数。多个 init 函数按照它们的文件名顺序逐个初始化。应用初始化时初始化工作的顺序是,从被导入的最深层包开始进行初始化,层层递出最后到main包。不管包被导入多少次,包内的init函数只会执行一次。应用初始化时初始化工作的顺序是,从被导入的最深层包开始进行初始化,层层递出最后到main包。但包级别变量的初始化先于包内init函数的执行。

问:Go中uintptr和unsafe.Pointer的区别?

答:unsafe.Pointer是通用指针类型,它不能参与计算,任何类型的指针都可以转化成 unsafe.Pointer,unsafe.Pointer 可以转化成任何类型的指针,uintptr 可以转换为 unsafe.Pointer,unsafe.Pointer 可以转换为 uintptr。uintptr是指针运算的工具,但是它不能持有指针对象(意思就是它跟指针对象不能互相转换),unsafe.Pointer是指针对象进行运算(也就是uintptr)的桥梁。

问:知道 golang的内存逃逸吗?什么情况下会发生内存逃逸?

答: 1)本该分配到栈上的变量,跑到了堆上,这就导致了内存逃逸。 2)栈是高地址到低地址,栈上的变量,函数结束后变量会跟着回收掉,不会有额外性能的开销。 3)变量从栈逃逸到堆上,如果要回收掉,需要进行 gc,那么gc 一定会带来额外的性能开销。编程语言不断优化gc算法,主要目的都是为了减少 gc带来的额外性能开销,变量一旦逃逸会导致性能开销变大。 内存逃逸的情况如下:

  1. 方法内返回局部变量指针。
  2. 向 channel 发送指针数据。
  3. 在闭包中引用包外的值。
  4. 在 slice 或 map 中存储指针。
  5. 切片(扩容后)长度太大。
  6. 在 interface 类型上调用方法。

这次先给大家整理21问,后面还有还会有第二篇。

参考文献:

https://zengzhihai.com https://www.bookstack.cn/read/golang_development_notes/zh-9.13.md 书籍《go专家编程》

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-04-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 利志分享 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 问:Go函数参数传递方式?
  • 问:Go函数参数传递是值传递,为什么map,slice,chan可能在函数内被修改?
  • 问:讲讲Go的slice底层数据结构和一些特性?
  • 问:讲讲Go的map底层数据结构和一些特性,map是否是线程安全?
  • 问:讲讲Go的chan底层数据结构和主要使用场景
  • 问:讲讲Go的select底层数据结构和一些特性?
  • 问:讲讲Go的defer底层数据结构和一些特性?
  • 问:Go 中解析 tag 是怎么实现的?反射原理是什么?
  • 问:讲讲Go的GPM模型
  • 问:讲讲Go的GC回收机制
  • 问:讲讲Go中主协程如何等待其余协程退出
  • 问:Go中的make和new的区别?
  • 问:Go中context 结构是什么样的?使用场景和用途
  • 问:Go中 rune 类型
  • 问:Go如何捕获异常
  • 问:Go语言中不同的类型如何比较是否相等?
  • 问:Go语言uint32最大值加1会怎么样?
  • 问:协程,线程,进程的区别?
  • 问:Go中init函数的特征
  • 问:Go中uintptr和unsafe.Pointer的区别?
  • 问:知道 golang的内存逃逸吗?什么情况下会发生内存逃逸?
    • 参考文献:
    相关产品与服务
    数据保险箱
    数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档