golang常见面试题

1. go语言有哪些优点、特性?

语法简便,容易上手。

支持高并发,go有独特的协程概念,一般语言最小的执行单位是线程,go语言支持多开协程,协程是用户态线程,协程的占用内存更少,协程只独有自己的栈等一些资源。其他都是共享线程的。调度时可以极大减少上下文切换。

2. go的包管理

golang在1.11版本之前用的是GOPATH,项目代码要放在GOPATH/src目录下才可运行。但是GOPATH存在弊端,没有版本控制概念,go get的外部库,自己都不知道下载的什么版本,再多人协作开发时无法确定大家的外部库版本是否一致,会出大问题。

golang1.11后推荐使用go mod 管理,go env 下设置 GO111MODULE = on 打开。支持依赖升降级。


3. make和new 的一些区别

new 可以初始化声明所有类型,返回的是一个指针类型。对应的是该类型的空值

make 只能用来初始化 map slice channel 的,返回的就是这个类型。对应的是该类型的零值

new的用法不常见,一般使用 var 或 :=


4. channel 是可以被 close 的,之后还可以读写吗?

不管是有无缓冲channel。关闭后都可以读,如果缓冲区里还有数据,先将缓冲区数据读完后,在读读的是channel 传输类型的 零值。

不管是有无缓冲channel,关闭后都不可以写,写会panic


5. channel有什么用

channel 用来做协程之间的通信。go语言提倡不要用共享内存来通信,要用通信的方式来共享内存。channel就是利用通信的方式实现对资源的访问。

无缓冲channel可以用来做协程间的同步

有缓冲channel可以用来做消息队列

6. channel的底层结构?接收、发送消息的过程?

channel底层是一个结构体,里面包含一个环形缓冲区,还有接收等待队列和发送等待队列。分别存放等待写消息的协程队列和读消息的协程队列,还有一个互斥锁,用来保证channel的线程安全,防止多个协程并发读写可能导致的问题。

当一个协程向channel写数据,如果是无缓冲通道,会将其加入到消息等待发送队列中,等待一个协程向channel读数据

读数据时,会先看缓冲区,如果没有再唤醒消息等待发送队列的第一个协程拿到信息,如果没有加入到消息等待接收队列,等待一个协程向channel写数据

7. channel 并发安全吗?

channel并发安全,底层结构体中有一个互斥锁,在并发环境下可以自动实现加锁解锁操作。


8. 怎么样输出一个有序的map

用slice存放map的key值,对slice进行排序,然后按照slice顺序查找map key value

用golang的数据结构 list来实现,list是一个链表。map[key]value value存放链表节点,写入map的时候同时按照顺序写入list中,顺序遍历时,只需要遍历链表即可。可以达到有序效果。


9. map在传参时的类型

golang传参都是值传递,只是对于map chan 等类型,他们在进行值拷贝的时候,调用的makechan 和 makemap 方法返回的都是指针类型的变量。导致操作的其实是同一个内存。

10. 可以直接对map取地址吗?

对map可以取址,但是对于map的key value不能取址,map不支持这种操作,当某种条件达到后,map会做增量扩容或者等量扩容操作。这时候每个key 的地址都可能发生变化。因此获取map key  value地址的操作是无意义的。

11. map底层结构

map底层是一个结构体,hmap,hmap中包含 元素的个数,桶的个数,以及指向桶数组的指针,每一个桶是一个bucket,bucket在go中用的是 bmap结构体,每个bucket可以存放 8个键值对,哈希值低八位相同的键存入bucket时会将高八位存储在tophash数组里。data 区域存放的key-value数据是按照 keykeykey valuevaluevalue存放的,overflow指针指向下一个bucket桶,通过链表将所有冲突的键连接起来。

12. 哪些数据类型不能作为map里面的key,哪些可以,有没有什么评判标准?

无法比较的数据类型都不能作为map里面的key。

基本数据类型都可作为key,指针也可以,因为指针比较的是地址。 数组也可以,结构体也可以

不可比较 map slice function

13. map是并发安全的吗?怎么实现并发安全?

map不是并发安全的,底层结构体没有用互斥锁进行加锁解锁操作。

如何实现并发安全,1.map+读写锁 (推荐)   2.用sync.Map


14. sync.Map如何解决并发问题?

sync.Map底层结构体中嵌入了一个互斥锁。当某个协程对map进行写操作时,会上写互斥锁,其他协程看到锁,于是会阻塞。当某个协程对map进行读操作时,会上读锁,但是读锁可以共享,所以其他协程也可以对map进行读操作。


15. gmp模型(局部饥饿、全局饥饿、全局缓存队列等)

最开始的时候,大多用线程池,开一定量的线程,当有工作任务到来时,会拿出一个线程处理,但当因为发生系统调用而阻塞时,线程池中可工作的线程就少了,线程池的性能就降低了。于是有了GMP模型。

G是协程,用户态线程

本地队列:每个P都有本地队列,本地队列中存放的是G

全局队列:当本地队列都满了,新来的G会优先加入本地队列中,本地队列满了会把本地队列的一半加入到全局队列

M是工作线程,用来处理协程的

P是处理器,默认数量是内核数,M通过获取P来处理P中的G

15. go有了协程之后,那它的线程是怎么调度的?(GMP模型)M最多多少个?

有调度策略:线程正常情况下会和P绑定,处理P中的本地队列G,但当处理G时,由于系统调用导致阻塞时,会触发hand off,M会和 P 解绑,把P转移给其他的空闲M,如果没有就创建一个新的M处理。如果M将P中的队列G处理完了,就会从其他P的本地队列中偷取一半协程来运行,如果其他都没有,就会从全局队列中选择一批来处理。

M最多10000个,在初始化运行时会设置最大值。但因为M需要获取到P才能处理G,P的数量是很有限的,所以M不会很多。

16. 线程数量和什么有关系?线程数量是无限大的吗?

线程数量和程序运行时,设置的最大M有关,默认是10000,并且跟P有关系,因为M必须要拿到P才能处理G。而且如果线程处理的阻塞的G完成后,可能会被销毁。不能无限大


16. slice深度拷贝

copy方法时,或者append触发扩容机制时都是深度拷贝。拷贝的是数据本身,新创建的对象和源对象不共享内存,会另开辟一个新的内存地址,值修改时不会影响源对象值。

17. slice和数组有什么区别?

slice 结构体中包含了 一个指向底层数组的指针还有slice的长度及容量

数组无法扩容,切片可以动态追加

数组声明时需要指定长度,切片不需要

数组是值类型的,切片是引用类型的


18. slice如何扩容?

1.18版本之前,当切片容量1024之前,每次扩容都是原来数组的二倍,当切片容量到1024之后,每次扩容都是原数组1.25倍

1.18版本,觉得扩容比例一下从2变成1.25有些不平滑,于是调整扩容机制,在容量小于256时,是2倍扩容,当容量大于256时,增加 (原来容量+3*256)/4。逐渐接近1.25倍,更平滑


19. interface{}怎么用

interface{}可以用来实现多态和符合设计模式的原则 依赖倒转 面向接口编程。 写一个接口,里面有要实现的方法,定义一个结构体,实现接口中的方法,然后初始化时,声明一个接口对象并指向具体的结构体,这样实现了面向接口编程,解耦。

也可以用来做强制类型转换,断言。interface.(type)


20. 说一下go的继承

go中的继承是通过结构体嵌套实现的,子结构体嵌套父结构体。初始化子结构体后,就可以用父结构体的方法。


21. go中哪些变量是不能比较的

map slice function 


22. golang强类型,弱类型?

golang是强类型语言,不会隐式的进行数据类型的转换。在速度上可能略逊弱类型语言,但是严谨性又避免了不必要的错误


23. golang并发控制?

可以用 Sync.WaitGroup实现  WaitGroup有三个方法,Add,Done,Wait。用来控制计数器数量。

可以用channel实现,goroutine之间通过channel通信,如果多个goroutine都写入channel,这时候通过缓冲区就可以实现并发控制,当缓冲区满时,后面的goroutine的写操作会被阻塞。


24. 一个go-routine最小占多大内存空间?

2kb


25. context类型有哪些?Context的作用是什么?context如何实现cancel的?

context主要用来在协程间传递关闭信号、信息的。可以控制多级协程

空context、cancelCtx、valueCtx、timerCtx。cancelCtx可以用来做协程的断开操作。




27. 正常模式和饥饿模式?

针对于互斥锁的,Mutex。正常模式下请求锁的协程要按照先入先出顺序排队,以此被唤醒,唤醒后还要与新请求锁的协程进行竞争,因为新请求的协程有优势,他们正在CPU上运行或者数量比较多。新唤醒的协程很难获取到锁,于是又会加到队列头部,如果一个等待的协程超过1ms仍未获取到锁,就会进入到饥饿模式。

饥饿模式下,互斥锁所有权会直接从解锁的协程转移到队首的协程,并且新到达的协程不会尝试获取锁而是加到队列尾部。如果一个等待协程获取到锁并且满足下面两个条件只一,就会回到正常模式。是队列的最后一个协程,等待时间小于1ms。

正常模式有更好的性能,饥饿模式可以避免尾部延迟这种情况


28. 用Go实现一个死锁

死锁存在四个条件,互斥,占有且等待,循环等待,不可强占用

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)
	var x sync.WaitGroup
	x.Add(2)
	go func() {
		ch2 <- 1
		fmt.Println(<-ch1)
		x.Done()
	}()
	go func() {
        ch1 <- 1
		fmt.Println(<-ch2)
		x.Done()
	}()
	x.Wait()
}


29. gc的了解

gc是垃圾回收机制,为了对堆栈上使用完的对象及时回收。防止内存溢出。最初1.3版本之前使用的是标记清除法,GC时会暂停程序进行标记清楚操作。STW时间很长。为了优化STW,1.3版本后缩短了一些STW时间,回收对象的操作放在STW时间之外。1.5版本时为了降低STW的时间,使用三色标记法+插入/删除屏障实现。三色标记法是在GC开始时,从根节点开始遍历所有可达对象,第一次遍历到的对象置灰,第二次遍历到的对象置黑。当没有灰色对象是开始回收对象。但是这样存在问题,当新创建了一个白色对象时,被一个黑色对象引用了,此时会造成刚创建的对象也被回收。还有当删除一个对象时,但是后面的对象还想要使用,但是断掉后后面的对象也会被回收。针对这两种情况提出了两个设计原则,强三色不变式和弱三色不变式,强三色强调黑色对象不能引用白色对象。弱三色不变式强调当黑色对象引用白色对象是,要保证还有一个灰色对象引用白色对象。针对这两个原则,产出了插入屏障和删除屏障,插入屏障实现的是强三色不变式,当黑色对象引用白色对象时,会将白色对象变为灰色对象。删除屏障实现的是弱三色不变式,当删除灰色后面的白色对象,会把删除的白色对象置灰。当扫描完没有灰色对象后,会将栈对象置白,启用STW,从新扫描一遍栈上的可达节点,最终删掉白色对象。插入屏障仍然需要re-scan栈上的节点。删除屏障,删除效率低,这次要删除的对象要等下次才能删掉。于是1.8版本后,采用三色标记法+混合写屏障。混合写屏障结合了插入和删除屏障的优点,栈上的对象不启用GC,扫描时会将所有栈上对象和新建对象置黑,针对堆上对象,所有创建和删除的对象都置灰。体现的是变形的弱三色不变式。1.8版本极大减少了STW时间,但并不是完全没有,因为在GC三色标记法之前还要STW,开启辅助GC和写屏障,统计根对象的任务数量等。


30. 什么时候会触发 golang GC 呢?

手动触发:调用runtime.GC来手动触发

定期触发:最长两分钟触发一次GC

内存达到一定值:每当内存扩大一倍时启用GC


31. 内存泄漏

创建的资源没有正常释放造成内存一直占用。也无法被GC回收。内存泄漏一般是程序员申请资源后没有手动去释放资源,关闭通道,解锁等。

通过pprof工具排查

32. go里面声明一个变量,它是放在栈上还是堆上?

内存逃逸,局部变量从栈上逃逸到堆上发生了内存逃逸。 

对于指针类型,因为分配内存时不知道是否存在外部引用,于是就将内存分配在堆中,防止函数结束后局部变量被回收。

如果数据量太大,栈放不下就会逃逸到堆中

interface类型上调用方法,编译时不知道是如何实现的,就会放到堆中。


33. 子goroutine的panic会不会被父g捕获

panic只能捕获本协程内部的错误,无法捕获子协程的错误

34. defer的先后顺序

defer满足栈的结构,先入后出。

35. defer什么情况下可以修改函数的返回值?

defer和return知识,return分为两步操作,第一步先将返回值赋给return,在准备返回之前要执行defer函数,执行完后才将结果返回。 如果定义了具名返回值,并且在defer时对这个变量进行了处理。那return 这个变量时就会修改。


36. golang 如何做超时控制?

1.timeAfter 这个函数会返回一个通道,并且过一段时间后会自动向通道发送一个数据。

2.context 的 withTimeout 和 withDeadline 方法,当到规定时间或超过某一时间后,会调用cancel方法来关闭channel。可以做超时控制


37. select 一般使用在什么场景

select一般与通道连用,用来监听多个通道的状态。

可以在多个通道中选择一个可用的操作来执行,如果没有一个通道准备就绪,select会执行defaule语句或者继续等待。

38.go程序运行顺序?

从main包开始执行,首先会导入引用的包,如果这个包里有init函数,会先执行这个包的init函数。

如果在main包定义了init函数,Go运行会在包导入后,全局变量初始化之前调用init函数。

调用之后会进行全局变量初始化操作,最终进入到main函数,是整个程序入口点。main函数执行后,程序会按照调用函数顺序依次执行其他函数。
 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值