golang 面试题
1. golang那些类型是引用类型,那些类型是值类型?
- 引用类型: 指针,map,slice,channel,interface
- 值类型: 非引用类型
Note: 对于interface类型,其原类型是引用类型那interface就也是引用类型,如果其原类型是值类型,那interface就也是值类型,取决与其原类型。
2. new和make的区别?
- new可以为任意类型的变量分配内存空间并初始化为对应类型的零值,返回变量的指针。
- make为map,slice,channel类型分配内存,并进行初始化,返回对应类型的引用。当new为map,slice,channel分配内存时,初始化为对应的零值为nil,当我们操作nil的map,slice或channel时就会因为空指针而panic。
3.golang切片和数组的区别?
切片和数组都是用来存储同一类型的数据集合,但是数组是值类型,而切片是引用类型,并且数组不可动态扩展长度,而切片可以自动扩容。切片的底层实现是通过数组来实现的,切片还比数组多了一个cap容量的概念。
4.切片的扩容方式?
当所需容量大于当前容量,就会扩容,当所需容量大于当前容量的两倍时就会直接扩到所需容量,当len > 1024的,就会每次扩25%直到扩到满足所需容量为止,当长度小于1024的时候,就每次扩两倍容量。并且所需容量不同类型的计算方式还不一样,但是可以保证计算出来的所需容量一定大于或等于真实的所需容量。一个int32类型的切片,其cap为0,len为0,一次性添加5个数据的之后,这时cap会为6,而如果是int16类型的则会为8,真实所需的容量是5。
5.切片扩容所引发的问题
当切片扩容时,会开辟一块新的内存空间,把老地址的切片的内容复制到新地址当中,并让原指针指向新的地址空间。避坑方法:1.使用copy函数,copy一个全新的切片 2.和append一样,去接收所返回的切片
6. 什么是内存逃逸
函数中的一个变量,如果其作用域没有超过该函数,那么该函数的内存就会在栈上分配,否则就会在堆上分配。一个变量的内存到底是在栈上分配,还是在堆上分配,取决于编译器做完逃逸分析之后决定的。
7.什么是深拷贝,什么是浅拷贝
深拷贝就是完全复制一个新的对象,新对象与原对象的存储空间完全不一样,而浅拷贝就是复制一个指针,指针仍然指向原对象的内存空间。在golang中,当我们调用一个函数,传引用类型的时候或者是指针都是浅拷贝,而传其他类型都是深拷贝。
8.调用一个函数传值还是传结构体?
调用一个函数,通常传的是指针,避免深拷贝带来的效率和内存上的消耗,除非我不希望该方法改变我结构体中的内容。接受的话一般接受结构体,避免内存逃逸带来gc上的压力。
Note: 接收参数,gc压力和深拷贝的开销取一个平衡点,其实怎么说应该都不算错,关键是要说明白你为什么要这么做。
9.什么是channel?
channel即管道,是golang的重要核心之一,是golang中协程通信的方式之一。Golang的并发哲学,不要通过内存共享来通信,而是通过通信来实现内存共享,其具体的体现就是channel。传统的mutex锁都是通过共享临界资源区来实现通信,而golang支持通过channel来进行通信,从而实现内存共享。
10.channel有哪几种
channel分为两种,带缓冲的channel和不带缓冲的channel
- make(chan int),这样创建的是不带缓冲的chan
- make(chan int, 1),这样创建的是带一个缓冲的chan
11.介绍一下channel的结构体组成(3_2021_11_3)
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
这里需要了解几个重要的属性,一个是buf指向存储数据的数组,sendx发送数据的索引,recvx接收数据的索引,sendq发送数据的等待队列,recvq接收数据的等待队列以及lock
当一个协程发送数据到带缓冲的channel时,把数据存储在缓冲数组的那个位置由sendx决定。如果缓冲池满了,就会加入到sendq等待队列当中,由接收的协程负责将其唤醒。
如何保证channel的并发安全,就是使用互斥锁来保证的。
12.向channel里面发送数据的逻辑
向channel里面发送数据分为以下几步:
- 首先看接收区的等待队列是否有正在等待的receiver,如果有的话,则直接把数据发送给receiver,并将其唤醒
- 然后看是否有缓冲区,如果缓冲区没满的话,则直接把数据放到缓冲区
- 如果是非阻塞的则直接返回,否则加入到发送区的等待队列当中去
13.goroutine接收的逻辑是怎么样的?
goroutine接受的逻辑和发送的逻辑差不多,分为以下几步:
- 首先查看sendq队列中是否有等待的g,如果有的话,则直接把等待的g中的数据取出,然后将其唤醒,返回即可
- 然后查看是否有缓冲区,缓冲队列是否有数据,如果有的话则直接从缓冲区中拿取数据
- 否则查看是否阻塞,如果阻塞则加入到recvq队列当中,否则直接返回
14.golang的select当有多个goroutine准备就绪,它是如何选择的?
select语句是用来处理与channel IO相关的逻辑,当有多个channel准备就绪的时候,其是伪随机选择一个goroutine来接收,然后执行相关的语句块。
Note: select关键字常用于和goroutine超时相关的逻辑设计。
15.golang什么时候会panic?
这里总结了8种,应付面试官应该是够了
- nil空指针异常
- 数组切片越界
- 向未初始化的map赋值
- 并发的写map
- 关闭未初始化的channel,重复关闭channel,向关闭的channel写数据
- 死锁,channel只有发送而没有接收
- interface类型断言失败
- 递归死循环,堆栈溢出
16.子协程出现panic能在父协程使用recover()捕获吗?
不能,只能在子协程内部使用recover()捕获panic,协程只能捕获自己的panic。
17.什么样的panic不可恢复
- 并发读写map
- 递归死循环
- 死锁
18.unsafe.Pointer和uintptr是用来干什么的呢?
在golang中,为了安全性,是不允许指针像C++那样进行类型转换以及计算的,但是有些场景又必须要这么做怎么办呢?于是出现了unsafe.Pointer用于指针类型转换,比如*int64可以转换为*int64,出现了uintptr用于指针运算。
对于unsafe.Point有以下几点性质:
- 任意类型的指针都能转化成unsafe.Point
- unsafe.Point能转化成任意类型的指针
- unsafe.Point可以转化成uintptr
- uintptr可以转化成unsafe.Point
19.常用unsafe.Point和uintptr做什么呢,这么做有什么好处呢?
unsafe.Point常用于操作结构体的私有变量,以及类型转换。
好处就是golang中只有unsafe.Point能做到这个事,其他方法都做不到,反射的底层也是用unsafe.Point做的。
20.unsafe.Point和unintptr有什么坑呢?
千万要小心,不要为uintptr起一个中间变量,例如这样:
u := uintptr(unsafe.Pointer(student))
age := (*int)(unsafe.Pointer(u))
这是因为当发生gc的时候,可能会修改变量的内存地址,同时也会修改指向该变量的指针指向新的地址。但是uintptr是一个整数,其不是一个指针,因此在gc修改变量的时候,可不会修改它的值,他还指向原来的地址,然后转化成unsafe.Point进行操作,当然会报错。