Let`s GO

面经、面试题

Posted by kzdgt on Sunday, March 19, 2023

深计院

  1. 说说new和make

    • make和new都是golang用来分配内存的內建函数,make 既分配内存,也初始化内存。new只是将内存清零,并没有初始化内存。

    • make返回的还是引用类型本身;而new返回的是指向类型的指针。

    • make只能用来分配及初始化类型为slice,map,channel的数据;new可以分配任意类型的数据。

  2. init的顺序,如果一个包有多个init会怎么样

    • 每个package可以定义多个init函数,甚至在同一个go文件也可以有多个init函数。
    • 如果一个包没有import其他包,则多个init按出现顺序初始化
    • 同一个包多个文件都有init函数则按文件名顺序初始化
    • 一般go fmt的话,会对import进行排序,这样子保证初始化行为的可再现性
    • 如果一个包有import其他包,则按依赖顺序从最里层包开始初始化

    聊聊golang的包init - 腾讯云开发者社区-腾讯云 (tencent.com)

  3. 说说for…range

    • range后面的元素会值拷贝,如果是数组,因为数组是值类型,对value操作不会影响到数组,可以使用数组指针,或者数组转为切片;

    • 遍历map是无序的

    【GO语言】For 循环三种常见陷阱_哔哩哔哩_bilibili

  4. for…range channel会怎么样

    通常我们会选择使用for range循环从通道中接收值,当通道被关闭后,会在通道内的所有值被接收完毕后会自动退出循环。上面那个示例我们使用for range改写后会很简洁。

    func f3(ch chan int) {
    	for v := range ch {
    		fmt.Println(v)
    	}
    }
    

    **注意:**目前Go语言中并没有提供一个不对通道进行读取操作就能判断通道是否被关闭的方法。不能简单的通过len(ch)操作来判断通道是否被关闭。

  5. 为什么slice是nil也可以直接append

    对于nil slice,append会对slice的底层数组做扩容,通过调用mallocgc向Go的内存管理器申请内存空间,再赋值给原来的nil slice。

  6. slice扩容规则

    切片的容量是怎样增长的 | Go 程序员面试笔试宝典 (golang.design)

    注意区分版本,以及需要扩容的几种情况:

    go1.18以前

    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
    	newcap = cap
    } else {
    	if old.len < 1024 {
    		newcap = doublecap
    	} else {
    		// Check 0 < newcap to detect overflow
    		// and prevent an infinite loop.
    		for 0 < newcap && newcap < cap {
    			newcap += newcap / 4
    		}
    		// Set newcap to the requested cap when
    		// the newcap calculation overflowed.
    		if newcap <= 0 {
    			newcap = cap
    		}
    	}
    }
    
    • 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。

    • 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),

    • 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)

    • 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。

    go1.18以后

    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
    	newcap = cap
    } else {
    	const threshold = 256
    	if old.cap < threshold {
    		newcap = doublecap
    	} else {
    		// Check 0 < newcap to detect overflow
    		// and prevent an infinite loop.
    		for 0 < newcap && newcap < cap {
    			// Transition from growing 2x for small slices
    			// to growing 1.25x for large slices. This formula
    			// gives a smooth-ish transition between the two.
    			newcap += (newcap + 3*threshold) / 4
    		}
    		// Set newcap to the requested cap when
    		// the newcap calculation overflowed.
    		if newcap <= 0 {
    			newcap = cap
    		}
    	}
    }
    

    这么做的目的

    在1.18中,优化了切片扩容的策略,让底层数组大小的增长更加平滑: 通过减小阈值并固定增加一个常数,使得优化后的扩容的系数在阈值前后不再会出现从2到1.25的突变,而是2、1.63、1.44这样。可以看到,Go1.18的扩容策略中,随着容量的增大,其扩容系数是越来越小的,可以更好地节省内存。我们可以试着求一个极限,当oldcap远大于256的时候,扩容系数将会变成1.25。

    如果只看前半部分,现在网上各种文章里说的 newcap 的规律是对的。

    现实是,后半部分还对 newcap 作了一个内存对齐,这个和内存分配策略相关。进行 内存对齐之后,新 slice 的容量是要 大于等于 按照前半部分生成的newcap

    之后,向 Go 内存管理器申请内存,将老 slice 中的数据复制过去,并且将 append 的元素添加到新的底层数组中。

    最后,向 growslice 函数调用者返回一个新的 slice,这个 slice 的长度并没有变化,而容量却增大了。

    —-深度解密Go语言之Slice - Stefno - 博客园 (cnblogs.com)

  7. map的key为什么是无序的

    key 为什么是无序的 | Go 程序员面试笔试宝典 (golang.design)

    map 的实现原理 | Go 程序员面试笔试宝典 (golang.design)

  8. 如何有序地遍历map

    对key排序,再遍历

  9. slice 和 map 分别作为函数参数时有什么区别?

    Go语言 函数传参时的不同

  10. slice可以做map的key吗

    在golang中,什么类型才可以作为map key? 在golang官方博客中找到以下说明:

    map key 必须是可比较的类型,语言规范中定义了可比较的类型:boolean, numeric, string, pointer, channel, interface, 以及仅包含这些类型的struct和array 。不能作为map key的类型有:slice,map, function

    在这里看出,array具有可比较性,可以作为map key,而slice不具有可比较性,所以不能作为map key。那这又是为什么? array是值类型,不同的array不会相互影响。对a1修改不会影响到a0。 slice指向底层的array,两个不同的slice可能指向同一个底层array。对s1修改会影响到s0。

  11. map是不是并发安全的,如果要设计一个并发安全的map,要怎么设计

    不是并发安全的,在查找、赋值、遍历、删除的过程中都会检测写标志,一旦发现写标志置位(等于1),则直接 panic。加锁

    map 是线程安全的吗 | Go 程序员面试笔试宝典 (golang.design)

  12. channel是线程安全的吗?为什么或者怎么实现的

    channel的底层实现中,hchan结构体中采用Mutex锁来保证数据读写安全。在对循环数组buf中的数据进行入队和出队操作时,必须先获取互斥锁,才能操作channel数据

  13. Go channel发送和接收什么情况下会死锁?

    Go语言基础之并发 | 李文周的博客 (liwenzhou.com)

    Go语言 channel进阶

  14. 用过什么锁

    Go语言中的互斥锁和读写锁(Mutex和RWMutex)

  15. 悲观锁、乐观锁

    乐观锁对应于生活中乐观的人总是想着事情往好的方向发展,悲观锁对应于生活中悲观的人总是想着事情往坏的方向发展。这两种人各有优缺点,不能不以场景而定说一种人好于另外一种人。乐观锁和悲观锁是两种思想,用于解决并发场景下的数据竞争问题。

    乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。

    因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。

    悲观锁:悲观锁在操作数据时比较悲观,认为别人会同时修改数据。

    因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。

    面试官灵魂4连问:乐观锁与悲观锁的概念、实现方式、场景、优缺点? - 知乎 (zhihu.com)

  16. 说说原子操作、单例模式

    原子操作由底层硬件支持,而锁则由操作系统的调度器实现。锁应当用来保护一段逻辑,对于一个变量更新的保护,原子操作通常会更有效率,并且更能利用计算机多核的优势,如果要更新的是一个复合对象,则应当使用atomic.Value封装好的实现。

    Golang 五种原子性操作的用法详解 - 腾讯云开发者社区-腾讯云 (tencent.com)

    单例模式指仅允许创建一个对象的设计模式。它通常应用于控制对某些资源的访问,例如数据库连接、线程池等等。通过单例模式,可以确保系统中只存在唯一一个实例,并提供一个全局访问点,方便其他对象使用。

    Go语言单例模式详解 - 掘金 (juejin.cn)

  17. GMP模型,抢占模式

  18. 内存溢出有没有分析过,哪些场景会内存溢出,如何分析

    Golang内存泄漏场景及解决方案_8023之永恒的博客-CSDN博客

  19. 多返回值是如何实现的

    Go 传参和返回值是通过 FP+offset 实现,并且存储在调用函数的栈帧中。FP 栈底寄存器,指向一个函数栈的顶部;PC 程序计数器,指向下一条执行指令;SB 指向静态数据的基指针,全局符号;SP 栈顶寄存器。

    深入分析golang多值返回以及闭包的实现 - 腾讯云开发者社区-腾讯云 (tencent.com)

  20. defer和return的顺序

    • 多个defer的执行顺序为“后进先出”;

    • defer、return、返回值三者的执行逻辑应该是:return最先执行,return负责将结果写入返回值中;接着defer开始执行一些收尾工作;最后函数携带当前返回值退出。

  21. 协程池/线程池有用过吗,或者自己写过吗,怎么实现的

  22. 堆和栈

    • 栈区(stack):由编译器自动分配和释放,存放函数的参数值、局部变量的值等。其操作方式类似于数据结构中的栈。
    • 堆区(heap):一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。它与数据机构中的堆是两回事,分配方式类似于链表。

    一文搞懂堆和栈的区别 - 腾讯云开发者社区-腾讯云 (tencent.com)

GPT提问

  1. 请问Golang中的文本输出函数是什么?简要说明它们的不同。

    Golang中的文本输出函数主要在fmt这个包里面,除了Print系列函数(如Println、Printf)之外,还有Sprint系列函数(如Sprintln、Sprintf)和Fprint系列函数(如Fprintln、Fprintf)。

    其中Print系列函数是输出到标准输出流中,Sprint系列函数是返回字符串结果,Fprint系列函数需要指定输出流,比如文件、网络连接。

    PrintPrintln 的区别在于 Println 会在输出内容的最后自动加上一个换行符 “\n”。而 Printf 使用占位符输出内容,占位符由一个百分号和一个字符组成(例如 %d 表示输出一个整数)。在占位符中,还可以使用其他选项,比如宽度、精度等。

  2. 请问你如何实现一个goroutine池?并描述一下其实现原理。

    golang设计一个goroutine池

  3. 你对Golang中的并发机制有了解吗?能否介绍一下Golang的并发模型和如何实现并发编程?

    Golang是一门天生支持并发的编程语言,在并发编程方面有着独特的设计思想和实现机制。

    Golang中的并发模型是基于Goroutine和Channel两个概念来实现的。Goroutine是一种轻量级的线程,由Go语言运行时管理。与操作系统线程相比,Goroutine 更轻量级、更高效,可以高效地利用多核CPU的性能。Goroutine 的调度是由Go语言运行时负责管理的,因此,程序员无需考虑线程调度的复杂性。

    而Channel是Golang中实现 Goroutine 间通信的关键。它类似于管道,可以把一个 Goroutine 的输出和另一个 Goroutine 的输入连接起来。Channel 保证了多个 Goroutine 之间的同步和数据传递的安全性。通过 Channel 可以非常方便地实现两个或多个 Goroutine 之间的数据传递和同步。

    在并发编程方面,Golang提供了丰富的并发控制原语,如互斥锁、读写锁、原子操作等,来保证协程之间的并发安全。

    同时,Go语言还提供了高效的并发编程接口,如sync包、context包等,以便程序员更方便地实现多协程的并发编程。

    值得一提的是,Golang的并发编程模型对于大规模分布式系统的开发有非常好的支持,因为分布式系统中大量的I/O操作会导致CPU因等待I/O而处于空闲状态。而Golang通过Goroutine的轻量级特性,可以轻松地支持成千上万个Goroutine同时执行,从而高效利用CPU资源,提高系统的并发处理能力。

  4. 如果要设计一个并发安全的高性能缓存系统,您会如何实现?并请具体描述您的设计思路和实现细节。

    golang设计一个并发安全的高性能缓存系统

  5. 如何进行类型断言,以及类型断言的作用是什么?

    类型断言是Golang中一个非常常用的操作,它的作用是在运行时检查一个接口值的实际类型,并将其转换为该类型的值。这在需要用到接口类型却不知道实际类型的情况下非常有用。类型断言的语法格式如下:

    value, ok := interfaceVar.(Type)
    

    其中,interfaceVar 是一个接口类型的变量,ok 是一个布尔值,表示该变量是否可以转换为 Type 类型, value 是一个 Type 类型的变量。

    如果 interfaceVar 可以被转换为 Type 类型,那么 value 将会是 interfaceVar 的实际值,ok 将会是 true。如果 interfaceVar 不能被转换为 Type 类型,那么 value 将会是 Type 类型的零值(比如 int 类型的零值是 0),ok 将会是 false

    需要注意的是,这里的 Type 必须是一个具体的类型,而不能是一个接口类型或者其他抽象类型。

其它

  1. 谈谈go的内存对齐机制

    Go内存对齐机制

面试题收集

【建议收藏】整理Golang面试第二篇干货13问_golang_利志分享_InfoQ写作社区

我把面试问烂了的⭐MySQL面试题⭐总结了一下(带答案,万字总结,精心打磨,建议收藏)-CSDN博客

我把面试问烂了的⭐Redis面试题⭐总结了一下(带答案,万字总结,精心打磨,建议收藏)-CSDN博客

「真诚赞赏,手留余香」

kzdgt Blog

真诚赞赏,手留余香

使用微信扫描二维码完成支付