面试题精选--Golang篇

总结一些常考的 Golang 面试题,相关资料皆从网络上收集
切片 - Slice
同一 slice 上的切片其底层数组是同一个吗
同一切片上的切片共享相同的底层数组。在 Go 中,切片是对数组的一个引用,而不是值的拷贝。当你对一个切片进行切片操作时,产生的新切片将仍然引用相同的底层数组,而不会创建新的底层数组。这意味着,无论你如何对切片进行切割或修改,底层数组都会相应地被修改。
|
|
append 操作返回的底层数组会变吗
在使用 append 函数向切片中添加元素时,如果当前切片容量不足以容纳新元素,append 函数会分配一个新的底层数组,将原切片中的元素复制到新的数组中,并在新的数组中添加新元素。这意味着 append 操作可能会返回一个指向不同底层数组的新切片。
|
|
当切片容量足够时,append 函数会原地修改原切片,因此返回的切片仍然指向相同的底层数组。在这种情况下,原切片可能会被修改,但它仍然引用相同的底层数组。
如果对 slice 中的元素取指针,放到一个新的数组中,新数组中的值是什么样的
考察对 for...range...
的了解。
对于for _, v := range array
,每次都会对 v
进行赋值,所以最后取到的都是变量 v
的地址。
可通过下列两种方式获得所有元素的地址:
|
|
映射 - Map
Map 是并发安全的吗
不是,Map 不并发安全,如果同时写同一个 Map,会造成 panic
通道 - Channel
有缓冲与无缓冲的 Channel 之间的区别
主要体现在两个方面:阻塞行为和并发性能。
缓冲 channel:
- 缓冲 channel 允许在发送数据时,如果 channel 中还有可用的缓冲区,则发送操作不会被阻塞,直接将数据发送到缓冲区中。
- 在接收数据时,如果 channel 中有数据可用,则接收操作不会被阻塞,直接从缓冲区中读取数据。
- 当 channel 中的缓冲区满了时,发送操作会阻塞直到有其他 goroutine 从 channel 中接收数据,腾出缓冲区空间。
- 同样地,当 channel 中的缓冲区为空时,接收操作会阻塞直到有其他 goroutine 向 channel 中发送数据。
无缓冲 channel:
- 无缓冲 channel 发送数据时,发送操作会阻塞直到有其他 goroutine 准备好接收数据。
- 接收数据时,接收操作会阻塞直到有其他 goroutine 发送数据到 channel 中。
- 无缓冲 channel 的发送和接收操作是同步的,它们会导致发送和接收两个 goroutine 同时被阻塞,直到它们能够匹配到对应的操作。
- 无缓冲 channel 通常用于同步 goroutine 之间的通信,保证数据的可靠传输和同步。
上下文 - Context
什么是 Context
在 Go 语言中,context.Context
是一个用于在 Goroutine 之间传递请求相关值、取消信号和截止时间的标准方式。它主要用于控制 Goroutine 的生命周期,以及在并发环境下进行请求处理、超时控制和取消操作等。
context.Context
的核心方法包括:
context.Background()
:返回一个空的Context
,用作父Context
,通常在整个请求的生命周期中作为顶级的Context
使用。context.WithCancel(parent)
:返回一个带有取消信号的Context
和一个取消函数。当调用取消函数时,该Context
及其所有子Context
都会收到取消信号。context.WithDeadline(parent, deadline)
:返回一个带有截止时间的Context
。当到达截止时间时,该Context
及其所有子Context
都会收到取消信号。context.WithTimeout(parent, timeout)
:返回一个带有超时时间的Context
。当超过指定的超时时间时,该Context
及其所有子Context
都会收到取消信号。context.WithValue(parent, key, value)
:返回一个带有请求相关值的Context
。这些值可以在 Goroutine 之间传递,但不应该用于传递可选参数。
Context
的值应该是请求范围内的元数据,例如请求 ID、用户身份验证信息等。不应该用于传递可选参数,而应该通过函数参数传递。
context.Context
的主要用途包括:
- 取消信号传递:用于在 Goroutine 中传递取消信号,以便在需要时取消处理。
- 超时控制:用于在一定时间内进行请求处理,并在超时时取消处理。
- 请求范围值传递:用于在 Goroutine 之间传递请求相关值,例如请求 ID、用户身份验证信息等。
使用 context.Context
可以有效地控制 Goroutine 的生命周期,避免资源泄漏和 Goroutine 泄漏,并实现更健壮的并发程序。
并发模型 - Goroutine 与 GMP
什么是协程泄漏
协程泄露是指在使用协程时,由于某些原因导致协程无法被及时回收和释放,从而占用了系统资源而无法被重复利用的情况。协程泄露可能会导致内存泄露或者系统资源耗尽等问题,影响程序的性能和稳定性。
常见引起协程泄露的原因包括:
未关闭通道(Channel): 当一个协程向通道发送数据后,如果没有其他协程接收数据,通道将会一直阻塞,导致该协程无法退出,从而产生协程泄露。因此,在使用通道时需要确保及时关闭通道以释放协程。
循环引用: 如果协程持有某些资源,而这些资源又持有了对协程的引用,形成了循环引用,那么即使协程不再需要这些资源,也无法被回收,从而导致协程泄露。这种情况下,需要仔细检查资源的生命周期,确保适时释放资源。
阻塞操作: 协程在执行过程中可能会发生阻塞,例如等待 I/O 操作完成或者等待锁释放等,如果阻塞时间过长或者无法及时结束,可能会导致协程泄露。需要谨慎设计协程的执行逻辑,避免长时间的阻塞操作。
未处理异常: 如果协程发生了未捕获的异常,并且没有恢复或处理这些异常,那么协程可能会被永久性地终止而无法被回收,从而导致泄露。
什么是 GMP 模型
- Goroutine(Go 协程): Goroutine 是 Go 语言中的轻量级线程,它由 Go 运行时(runtime)调度和管理。Goroutine 可以看作是执行并发任务的独立单位,相较于传统的线程,Goroutine 的创建和切换成本非常低,因此可以高效地支持大量的并发任务。
- M(线程): M 代表着操作系统的线程(machine thread)。每个 M 都会关联一个线程,它负责执行 Goroutine。M 的数量是由 Go 运行时动态管理的,它会根据系统的核心数量等因素动态调整 M 的数量,以充分利用系统资源。
- P(处理器): P 是一种逻辑处理器,它负责调度和管理 Goroutine。P 的数量通常是固定的,并且与 CPU 核心数有关。P 会将 Goroutine 分配给 M,并负责调度 M 在 CPU 上执行。P 的数量可以通过 Go 语言的 GOMAXPROCS 环境变量来控制。
M 和 P 是一对一的吗
M 和 P 是多对多的关系,一个 P 可以管理多个 M,但一个 M 同一时间只能被一个 P 所关联。
处理器 P 如何管理多个线程 M
- 本地队列(Local Queue): 每个 P 都有一个本地队列,用于存储被当前 P 管理的 Goroutine。这个队列是针对当前 P 而言的,因此可以高效地进行操作,比如入队和出队。本地队列可以减少锁的竞争,提高并发性能。
- 全局队列(Global Queue): 所有 P 共享一个全局队列,用于存储没有被任何 P 管理的 Goroutine。当一个 P 的本地队列空了,它可以从全局队列中获取 Goroutine 来执行。全局队列通常会使用锁进行保护,因为多个 P 可能同时竞争全局队列中的 Goroutine。
- 空闲 M 列表(Idle M List): P 还会维护一个空闲 M 的列表。当 P 的本地队列满了或者被阻塞时,它可以从空闲 M 列表中获取一个空闲的 M 来执行 Goroutine。
P 通过这些队列来动态管理多个 M 的调度和执行,从而实现高效的并发调度。当一个 M 执行完成或者阻塞时,它会回到 P 的管理下,然后再次被分配 Goroutine 执行。
死循环的协程如何调度
- Preemption(抢占): Go 语言的运行时系统会在一定的时间间隔内(通常几毫秒)进行抢占调度,即使一个协程正在执行一个死循环,也会在一定的时间后将处理器让给其他协程执行。这种机制能够保证其他协程不会因为某个协程的死循环而无法执行。
- 系统调用或阻塞操作: 如果一个协程在执行死循环时发起了系统调用或者进行了阻塞操作(如等待 IO 完成),那么它会被 Go 运行时系统暂时挂起,直到系统调用完成或阻塞操作解除,从而让其他协程有机会执行。
- 手动控制: 在一些特殊情况下,可以手动控制死循环协程的执行。例如,可以使用 Go 语言的 select{} 语句结合 time.After 通道来设置一个超时,以便让死循环协程周期性地让出处理器,从而允许其他协程执行。但这种方法需要手动在死循环中加入超时检查的逻辑。
阻塞的协程如何调度
- 系统调用阻塞: 当一个协程执行系统调用(如文件读取、网络请求等)而导致阻塞时,Go 调度器会将该协程暂停,并尝试调度其他可运行的协程来继续执行。当系统调用完成后,阻塞的协程会重新被调度执行。
- 通道阻塞: 当一个协程试图发送或接收数据到或从一个无缓冲通道时,如果通道中没有对应的接收方或发送方,该协程会被阻塞。在这种情况下,Go 调度器会暂停被阻塞的协程,并尝试调度其他可运行的协程执行,直到通道操作可以完成。
- 等待组(WaitGroup)阻塞: 当主协程等待一组其他协程完成时,通常会使用
sync.WaitGroup
进行同步。在这种情况下,主协程会调用WaitGroup.Wait()
方法阻塞等待,直到所有其他协程完成并调用WaitGroup.Done()
通知完成。Go 调度器会在这种情况下暂停主协程,并继续执行其他可运行的协程。
垃圾回收 - GC
Go 中的垃圾回收是怎样的
Go 语言的垃圾回收(Garbage Collection,GC)是一种自动管理内存的机制,它负责在程序运行时自动识别并释放不再使用的内存对象,以避免内存泄漏和提高内存利用率。
以下是 Go 语言的垃圾回收的一些特点和工作原理:
并发标记清除: Go 语言的垃圾回收器采用了并发标记清除(Concurrent Mark and Sweep)算法。在标记阶段,垃圾回收器会从根对象开始,遍历程序中的对象图,并标记出所有可达的对象。在清除阶段,垃圾回收器会扫描堆中的对象,清除所有未被标记的对象。这个过程是并发执行的,不会阻塞用户程序的执行。
三色标记法: Go 语言的垃圾回收器使用了三色标记法,将对象分为三种状态:白色、灰色和黑色。白色表示对象未被访问,灰色表示对象被标记但其子对象还未被标记,黑色表示对象及其子对象已被标记。在标记阶段,垃圾回收器通过遍历对象图,将对象从白色变为灰色,并将其子对象加入标记队列。在清除阶段,垃圾回收器清除所有白色对象。
分代回收: Go 语言的垃圾回收器采用了分代回收策略,将堆中的对象分为几个代(generation)。新分配的对象会被分配到年轻代,经过多次垃圾回收后仍然存活的对象会被晋升到老年代。由于年轻代的对象生命周期较短,因此可以采用更频繁的垃圾回收策略,而老年代的对象则采用更慢的垃圾回收策略,以提高性能。
写屏障: Go 语言的垃圾回收器使用写屏障技术,记录对象的写入操作,以便在垃圾回收过程中正确识别对象的引用关系。写屏障会在写入指针时触发,将被修改的对象标记为灰色,并将新写入的指针加入标记队列。
三色标记的过程
三色标记法是一种用于标记-清除(mark-sweep)垃圾回收算法的优化技术,它将对象的标记状态分为三种颜色:白色、灰色和黑色。以下是三色标记法的基本过程:
初始标记(Initial Mark): 在初始标记阶段,垃圾回收器从根对象开始,标记所有直接可达的对象为灰色,并将它们加入标记队列。根对象通常是全局变量、栈上的变量以及活跃的协程等。
并发标记(Concurrent Mark): 在并发标记阶段,垃圾回收器并发地遍历堆中的对象图,标记所有可达的对象为灰色,并将其子对象加入标记队列。如果发现新的灰色对象,将其标记为黑色,并将其子对象加入标记队列。
重新标记(Re-Mark): 在并发标记过程中,程序可能会产生新的对象,这些新对象可能会在标记过程中被修改,因此需要重新标记。重新标记阶段会对之前标记的所有灰色对象进行重新扫描,将其子对象加入标记队列,并标记为黑色。
清除(Sweep): 在清除阶段,垃圾回收器扫描整个堆,清除所有未被标记的白色对象,并将它们释放回内存池。清除过程是并发执行的,不会阻塞程序的执行。
其他
Go 中的 make 和 new 的区别
在 Go 语言中,make
和 new
都是用于创建数据结构的内置函数,但它们的使用场景和作用不同。
make:
make
函数主要用于创建 slice、map 和 channel 这三种引用类型的数据结构,并且返回的是一个初始化之后的数据结构。- 例如,使用
make
函数创建一个长度为 5 的整型 slice:s := make([]int, 5)
make
函数的签名为:func make(t Type, size ...IntegerType) Type
,其中t
表示数据结构的类型,size
表示数据结构的大小或者容量(对于 slice 和 channel)。
new:
new
函数用于创建某种类型的指针,并且返回该类型的指针的零值,即指向该类型的新分配的零值的指针。- 例如,使用
new
函数创建一个整型指针:p := new(int)
new
函数的签名为:func new(Type) *Type
,其中Type
表示要创建的类型。
总的来说,make
用于创建引用类型的数据结构,返回的是一个已初始化的数据结构;而 new
用于创建某种类型的指针,返回的是该类型的零值的指针。
在 defer 中修改了局部变量并 return,返回值为类型和(变量+类型)两种情况下会返回什么
在 Go 中,defer
语句中对局部变量的修改对函数的返回值没有影响。无论函数返回值是单个变量还是多个变量(包括变量及其类型),在 defer
中对局部变量的修改不会影响函数返回值。这是因为 Go 在函数返回时会将函数的返回值保存在栈中,并不会受到 defer
中对变量的修改影响。