Golang面试分享来了,为了帮助大家更好的面试,笔者总结一份相关的Golang知识的面试问题,希望能帮助大家。
Go的函数参数传递都是值传递。所谓值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。参数传递还有引用传递,所谓引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
答:因为Go里面的map,slice,chan是引用类型。变量区分值类型和引用类型。所谓值类型:变量和变量的值存在同一个位置。所谓引用类型:变量和变量的值是不同的位置,变量的值存储的是对值的引用。但并不是map,slice,chan的所有的变量在函数内都能被修改,不同数据类型的底层存储结构和实现可能不太一样,情况也就不一样。
答: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。)
答:golang中map是一个kv对集合。底层使用hash table,用链表来解决冲突 ,出现冲突时,不是每一个key都申请一个结构通过链表串起来,而是以bmap为最小粒度挂载,一个bmap可以放8个kv。在哈希函数的选择上,会在程序启动时,检测 cpu 是否支持 aes,如果支持,则使用 aes hash,否则使用 memhash。每个map的底层结构是hmap,是有若干个结构为bmap的bucket组成的数组。每个bucket底层都采用链表结构。hmap的结构如下:
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,快速试错后再进行完整对比
答:channel的数据结构包含qccount当前队列中剩余元素个数,dataqsiz环形队列长度,即可以存放的元素个数,buf环形队列指针,elemsize每个元素的大小,closed标识关闭状态,elemtype元素类型,sendx队列下表,指示元素写入时存放到队列中的位置,recv队列下表,指示元素从队列的该位置读出。recvq等待读消息的goroutine队列,sendq等待写消息的goroutine队列,lock互斥锁,chan不允许并发读写。 无缓冲和有缓冲区别: 管道没有缓冲区,从管道读数据会阻塞,直到有协程向管道中写入数据。同样,向管道写入数据也会阻塞,直到有协程从管道读取数据。管道有缓冲区但缓冲区没有数据,从管道读取数据也会阻塞,直到协程写入数据,如果管道满了,写数据也会阻塞,直到协程从缓冲区读取数据。 channel的一些特点
向channel写数据的流程:
向channel读数据的流程:
使用场景: 消息传递、消息过滤,信号广播,事件订阅与广播,请求、响应转发,任务分发,结果汇总,并发控制,限流,同步与异步
答:简介:go的select为golang提供了多路IO复用机制,和其他IO复用一样,用于检测是否有读写事件是否ready。linux的系统IO模型有select,poll,epoll,go的select和linux系统select非常相似。 数据结构如下: select结构组成主要是由case语句和执行的函数组成 select实现的多路复用是:每个线程或者进程都先到注册和接受的channel(装置)注册,然后阻塞,然后只有一个线程在运输,当注册的线程和进程准备好数据后,装置会根据注册的信息得到相应的数据。 select的特性
答:每个defer语句都对应一个_defer实例,多个实例使用指针连接起来形成一个单连表,保存在gotoutine数据结构中,每次插入_defer实例,均插入到链表的头部,函数结束再一次从头部取出,从而形成后进先出的效果。defer的规则总结:
答:Go中解析的tag是通过反射实现的,反射是指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力或动态知道给定数据对象的类型和结构,并有机会修改它。反射将接口变量转换成反射对象 Type 和 Value;反射可以通过反射对象 Value 还原成原先的接口变量;反射可以用来修改一个变量的值,前提是这个值可以被修改。
答: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 V1.3之前普通标记清除(mark and sweep)方法,整体过程需要启动STW,效率极低。GoV1.5三色标记法,堆空间启动写屏障,栈空间不启动,全部扫描之后,需要重新扫描一次栈(需要STW),效率普通。GoV1.8三色标记法,混合写屏障机制:栈空间不启动(全部标记成黑色),堆空间启用写屏障,整个过程不要STW,效率高。
答:Go的sync.WaitGroup是等待一组协程结束,sync.WaitGroup只有3个方法,Add()是添加计数,Done()减去一个计数,Wait()阻塞直到所有的任务完成。Go里面还能通过有缓冲的channel实现其阻塞等待一组协程结束,这个不能保证一组goroutine按照顺序执行,可以并发执行协程。Go里面能通过无缓冲的channel实现其阻塞等待一组协程结束,这个能保证一组goroutine按照顺序执行,但是不能并发执行。
答:make和new都是golang用来分配内存的內建函数,且在堆上分配内存,make 即分配内存,也初始化内存。new只是将内存清零,并没有初始化内存。make是用于引用类型(map,chan,slice)的创建,返回引用类型的本身,new创建的是指针类型,new可以分配任意类型的数据,返回的是指针。
答: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:超时控制:到某个时间点超时,过多久超时。
答:rune类型实质其实就是int32,在处理字符串及其便捷的字符单位。它会自动按照字符独立的单位去处理方便我们在遍历过程中按照我们想要的方式去遍历。
答:Go的异常处理主要通过defer func(){}()实现闭包,函数内if err :=revover();err!=nil{}来实现,if里面打印异常,关闭资源,或者退出此函数等。完整代码如下:
defer func() {
if err := recover(); err != nil {
// 打印异常,关闭资源,退出此函数
fmt.Println(err)
}
}()
答:像string,int,float interface等可以通过reflect.DeepEqual和等于号进行比较,像slice,struct,map则一般使用reflect.DeepEqual来检测是否相等。
答:溢出,报错
答:
答:一个包下可以有多个init函数,每个文件也可以有多个init 函数。多个 init 函数按照它们的文件名顺序逐个初始化。应用初始化时初始化工作的顺序是,从被导入的最深层包开始进行初始化,层层递出最后到main包。不管包被导入多少次,包内的init函数只会执行一次。应用初始化时初始化工作的顺序是,从被导入的最深层包开始进行初始化,层层递出最后到main包。但包级别变量的初始化先于包内init函数的执行。
答:unsafe.Pointer是通用指针类型,它不能参与计算,任何类型的指针都可以转化成 unsafe.Pointer,unsafe.Pointer 可以转化成任何类型的指针,uintptr 可以转换为 unsafe.Pointer,unsafe.Pointer 可以转换为 uintptr。uintptr是指针运算的工具,但是它不能持有指针对象(意思就是它跟指针对象不能互相转换),unsafe.Pointer是指针对象进行运算(也就是uintptr)的桥梁。
答: 1)本该分配到栈上的变量,跑到了堆上,这就导致了内存逃逸。 2)栈是高地址到低地址,栈上的变量,函数结束后变量会跟着回收掉,不会有额外性能的开销。 3)变量从栈逃逸到堆上,如果要回收掉,需要进行 gc,那么gc 一定会带来额外的性能开销。编程语言不断优化gc算法,主要目的都是为了减少 gc带来的额外性能开销,变量一旦逃逸会导致性能开销变大。 内存逃逸的情况如下:
这次先给大家整理21问,后面还有还会有第二篇。
https://zengzhihai.com https://www.bookstack.cn/read/golang_development_notes/zh-9.13.md 书籍《go专家编程》