当前位置:   article > 正文

程序员面试系列,golang常见面试题

程序员面试系列,golang常见面试题

原文链接

一、channel相关的面试题

(1)make(chan int, 1) 和 make(chan int)有区别吗

make(chan int, 1)make(chan int) 之间有区别。

  1. make(chan int, 1) 创建了一个有缓冲的通道,容量为1。这意味着通道可以缓存一个整数元素,即使没有接收方,发送操作也不会被阻塞,直到通道已满。如果没有接收方,发送操作会立即完成。如果通道已满,发送操作会被阻塞,直到有接收方接收数据。这种通道适用于发送方和接收方的速度不一致的情况。
  2. make(chan int) 创建了一个无缓冲的通道,容量为0。这意味着通道没有缓冲区,发送操作会阻塞直到有接收方接收数据,而接收操作也会阻塞直到有发送方发送数据。这种通道适用于同步操作,即发送方和接收方需要同步地进行数据交换。

因此,根据具体的需求和使用场景,选择适当的通道类型是很重要的。如果需要在发送和接收之间有一定的缓冲空间,可以使用有缓冲通道;如果需要同步操作,确保发送和接收的同步性,可以使用无缓冲通道。

(2) channel 底层怎么实现的

Go中的channel是一种用于在不同goroutine之间进行通信和同步的基本构造。它可以用于将数据从一个goroutine发送到另一个goroutine,也可以用于实现多个goroutine之间的同步。channel是一种线程安全的数据结构,可以保证多个goroutine之间的数据传输是安全的。

关于channel的内部实现原理,可以简要说明以下几点:

  1. channel的底层数据结构:channel底层是由一个带缓冲的队列实现的,当channel是无缓冲的时候,它的队列为空,发送和接收操作会直接阻塞,直到有另一个goroutine准备好接收或发送数据。当channel是有缓冲的时候,它的队列会在缓冲区未满时存储数据,发送和接收操作在缓冲区未满时不会阻塞。
  2. channel的同步操作:在没有缓冲的channel上进行发送和接收操作会导致发送方和接收方同步等待,直到两者都准备好,这种同步机制保证了通信的安全性。而在有缓冲的channel上进行发送操作只有在缓冲区满时才会阻塞,接收操作只有在缓冲区为空时才会阻塞。
  3. channel的内部锁:channel的实现中使用了内部锁来保证多个goroutine对channel的操作是线程安全的。
  4. channel的关闭:可以使用close()函数关闭channel,关闭后的channel无法再发送数据,但可以继续接收数据。关闭后的channel如果没有接收完所有数据,接收操作仍然可以继续,但会接收到零值。

二、GC相关的面试题

(1)Go GC有几个阶段

目前的go GC采用三色标记法混合写屏障技术。

Go GC有个阶段:

  • STW,开启混合写屏障,扫描栈对象;
  • 将所有对象加入白色集合,从根对象开始,将其放入灰色集合。每次从灰色集合取出一个对象标记为黑色,然后遍历其子对象,标记为灰色,放入灰色集合;
  • 如此循环直到灰色集合为空。剩余的白色对象就是需要清理的对象。
  • STW,关闭混合写屏障;
  • 在后台进行GC(并发)。

(2)GC的具体过程

Go语言的垃圾回收(Garbage Collection,简称GC)是自动内存管理的一种机制,用于自动检测和回收不再使用的内存,减轻了程序员手动管理内存的负担,提高了程序的健壮性和开发效率。

Go的GC过程可以简要概括为以下几个步骤:

  1. 标记阶段(Marking Phase):GC从根对象开始,递归遍历所有可达对象,并对可达对象进行标记。根对象包括栈上的变量、全局变量和程序中已分配的堆对象。
  2. 标记终止阶段(Mark Termination):当所有可达对象都被标记后,GC暂停程序的执行,执行标记终止阶段,该阶段用于处理在标记过程中有新对象产生的情况。
  3. 清除阶段(Sweeping Phase):在标记阶段标记过的对象会被保留,而未标记的对象会被判定为垃圾,需要被回收。清除阶段会遍历堆上的所有对象,将未标记的对象回收,释放其内存。
  4. 回收空闲内存(Heap Reclamation):在清除阶段完成后,Go的堆中会有一些内存空闲,回收空闲内存使其能够再次被分配给新的对象。

Go的GC采用了三色标记法(Tri-color Marking),即将对象分为三种颜色:白色、灰色和黑色。

  • 白色:表示该对象未被访问过,处于未知状态。
  • 灰色:表示该对象被访问过,但其子对象可能还未被访问,处于待访问状态。
  • 黑色:表示该对象被访问过,并且其子对象也已经被访问过,处于已访问状态。

GC会从根对象开始,将所有根对象标记为灰色,并将其放入待处理队列。然后从待处理队列中取出一个灰色对象,将其标记为黑色,并将其子对象标记为灰色,再放入待处理队列。重复该过程,直到待处理队列为空,所有可达对象都被标记为黑色。

值得注意的是,Go的GC使用了并发标记和并发清除的策略,即GC过程与程序的执行并发进行,减少了GC对程序执行的影响。同时,Go的GC还实现了增量标记和并发回收,将GC的时间分摊到多个小步骤,降低了GC造成的延迟。

(3)go的垃圾收集过程中,有没有可能一个对象,前一时刻是垃圾,下一时刻就不是垃圾?

在 Go 的垃圾收集过程中,是有可能发生一个对象在前一时刻被标记为垃圾(即不再被引用)而在下一时刻又不是垃圾(重新被引用)的情况。

这种情况被称为 “retracing”,也就是在垃圾收集器扫描对象图的过程中,有新的引用或修改了旧引用,导致之前被标记为垃圾的对象变为可达,从而避免了被回收。

Go 的垃圾收集器使用的是 “标记-清除”(Mark and Sweep)算法,其中 “标记” 阶段会遍历对象图,并标记所有可达的对象,而 “清除” 阶段则回收没有被标记的对象。

在并发的情况下,当垃圾收集器进行标记阶段时,程序可能在其他 goroutines 中产生新的对象引用或修改旧引用。这些引用的变化可能会导致之前被标记为垃圾的对象重新变为可达状态,从而不会被回收。

Go 的垃圾收集器使用了写屏障(Write Barrier)等技术来处理并发情况下的对象引用变化,以确保垃圾收集过程的正确性。但是,由于并发情况下的引用变化是动态的,垃圾收集器可能需要重新追踪对象,这就是 “retracing” 的原因。

虽然 “retracing” 存在,但 Go 的垃圾收集器已经经过精心设计和优化,以在大多数情况下提供高效的垃圾回收。对于大多数应用来说,不必过于担心 “retracing” 的问题,垃圾收集器会自动根据程序的运行情况动态地调整自己的行为,以达到尽可能高效的回收效果。

写屏障(Write Barrier)是一种在并发垃圾收集过程中用于处理对象引用变化的技术。在并发情况下,当一个 goroutine 修改了一个对象的引用关系时,其他的 goroutines 可能同时访问这个对象,而这时垃圾收集器正在对对象图进行标记。

写屏障技术的目的是确保垃圾收集器能够正确地跟踪引用关系,避免漏掉已经变为不可达的对象或错误地回收仍然可达的对象。

具体来说,写屏障技术在发生写操作时,会插入一些特殊的代码来通知垃圾收集器有一个引用关系的变化。这样,垃圾收集器就能够正确地追踪到新的引用关系,将新的引用标记为可达,或者取消旧的引用的标记。这样,在并发垃圾收集过程中,即使有其他 goroutines 在修改引用关系,垃圾收集器仍然能够正确地进行标记和回收。

Go 语言的垃圾收集器就使用了写屏障技术来处理并发情况下的对象引用变化。通过使用写屏障,Go 的垃圾收集器能够保证垃圾回收过程的正确性,并在大多数情况下提供高效的垃圾回收性能。

需要注意的是,写屏障技术可能会在一定程度上带来一些额外的开销,因为它需要在写操作时插入额外的代码。但是,这个开销通常是值得的,因为它保证了垃圾收集器的正确性和性能,并帮助 Go 语言实现高效的并发垃圾回收。

三、string相关的面试题

(1)go中的字符串拼接有哪几种,每种方式的效率如何

拼接字符串的方式有:+ , fmt.Sprintf , strings.Builder, bytes.Buffer, strings.Join

  1. “+”

使用+操作符进行拼接时,会对字符串进行遍历,计算并开辟一个新的空间来存储原来的两个字符串。

  1. fmt.Sprintf

由于采用了接口参数,必须要用反射获取值,因此有性能损耗。

  1. strings.Builder:

用WriteString()进行拼接,内部实现是指针+切片,同时String()返回拼接后的字符串,它是直接把[]byte转换为string,从而避免变量拷贝。

  1. bytes.Buffer

bytes.Buffer是一个一个缓冲byte类型的缓冲器,这个缓冲器里存放着都是byte

bytes.buffer底层也是一个[]byte切片。

  1. strings.join

strings.join也是基于strings.builder来实现的,并且可以自定义分隔符,在join方法内调用了b.Grow(n)方法,这个是进行初步的容量分配,而前面计算的n的长度就是我们要拼接的slice的长度,因为我们传入切片长度固定,所以提前进行容量分配可以减少内存分配,很高效。

性能比较

strings.Join ≈ strings.Builder > bytes.Buffer > “+” > fmt.Sprintf

四、slice相关的面试题

在Go语言的面试中,关于切片的问题是很常见的。以下是一些可能出现的面试题:

  1. 切片和数组的区别是什么?什么情况下使用切片,什么情况下使用数组?
  2. 切片的底层数据结构是什么?如何扩容切片的容量?
  3. 切片的长度和容量有什么区别?如何获取切片的长度和容量?
  4. 切片是否可比较?为什么?
  5. 如何判断两个切片是否相等?
  6. 如何在切片中插入和删除元素?
  7. 切片是否支持负索引?为什么?
  8. 切片的追加操作会发生什么?如何避免在追加时重新分配底层数组内存?
  9. 如何遍历切片?有哪些方法可以遍历切片的元素?
  10. 切片在函数间传递时是传值还是传引用?
  11. 如何使用切片进行栈和队列的操作?
  12. 切片是否可以作为函数的参数和返回值?
  13. 切片的零值是什么?如何判断一个切片是否为nil?
  14. 切片在并发编程中是否安全?
  15. 切片是否支持多维?如何创建多维切片?

(1)go中的数组和切片的区别是什么

在 Go 中,数组(Array)和切片(Slice)是两种不同的数据类型,它们具有一些区别。

  1. 大小固定 vs 大小可变:数组的大小在创建时就确定,并且无法改变,而切片的大小是动态的,可以根据需要进行扩展或缩减。
  2. 内存布局:数组是一段连续的内存空间,其中每个元素具有相同的类型和固定的大小。切片则是对底层数组的一个引用,包含了指向底层数组的指针、长度和容量。
  3. 传递方式:数组作为函数参数传递时,会进行值拷贝,即传递的是整个数组的副本。而切片作为函数参数传递时,传递的是底层数组的引用,对切片的修改会影响到底层数组。
  4. 动态性和灵活性:切片具有更大的灵活性和动态性,可以根据需要进行动态增长和缩减。切片支持切片操作、追加元素、删除元素等操作,使得处理动态数据集合更加方便。
  5. 声明方式:数组的长度必须在声明时指定,例如 [5]int 表示长度为 5 的整型数组。而切片可以使用 []Type 的方式声明,例如 []int 表示一个整型切片。

在实际应用中,切片更加常用,因为它提供了更多的灵活性和便利性。切片可以根据需求动态调整大小,并且在函数传递和返回时更加高效和方便。数组在特定场景下仍然有其用处,例如固定大小的数据集合或对内存布局有特殊要求的情况下。

(2)切片是可以比较的吗?为什么

在Go语言中,切片是不可比较的。切片是引用类型,包含指向底层数组的指针、切片的长度和容量。由于切片是动态长度的,即可以动态增长或缩减,因此在切片的比较过程中可能涉及到多个底层数组的元素,这使得切片的比较变得复杂。

在Go语言中,切片的比较是不允许的,因为切片的底层数据可能在内存中的不同位置,即使它们的元素相同,它们的指针地址也不相同。这就导致了在比较两个切片时,无法简单地通过比较指针地址或元素值来确定它们是否相等。

如果需要判断两个切片是否包含相同的元素,可以通过遍历切片的元素逐个比较来实现,但不能直接通过 == 操作符来比较整个切片。例如,可以使用循环或者使用reflect包来编写一个函数来判断两个切片是否相等,但是不能直接使用 s1 == s2 来判断两个切片是否相等。

总结:在Go语言中,切片是不可比较的,不能直接使用 == 操作符来比较两个切片是否相等,需要使用其他方法来进行切片的比较。

五、指针相关的面试题

(1)go中的new和make的区别是什么

在 Go 中,newmake 是两个用于创建数据结构的内建函数,它们有不同的使用场景和作用。

  1. new 函数:new 用于创建某种类型的指针,并返回该指针的零值。它接受一个参数,即要创建的类型,返回一个指向该类型的零值的指针。使用 new 函数适用于创建值类型(如结构体、整型、浮点型等)的指针。

    示例:ptr := new(int),在这个示例中,new(int) 创建了一个 int 类型的指针,并将其初始化为 int 类型的零值。

  2. make 函数:make 用于创建切片、映射和通道等引用类型的数据结构,并进行初始化。它接受两个或三个参数,第一个参数是要创建的类型,后面的参数根据类型的不同而有所不同。

    • 对于切片(slice):make([]T, length, capacity),其中 T 是切片的元素类型,length 是切片的长度,capacity 是切片的容量。
    • 对于映射(map):make(map[K]V, capacity),其中 K 是键的类型,V 是值的类型,capacity 是映射的初始容量。
    • 对于通道(channel):make(chan T, capacity),其中 T 是通道中元素的类型,capacity 是通道的缓冲区容量。

    make 函数适用于创建引用类型的数据结构,并且会进行初始化,返回一个已经初始化的引用类型的值。

总结:new 用于创建值类型的指针,并返回指针的零值;make 用于创建引用类型的数据结构,并进行初始化。根据不同的需求和数据类型,选择适当的函数来创建相应的数据结构。

(2)如果使用new初始化map、slice、channel会怎么样

在 Go 中,使用 new 初始化 mapslicechannel 会得到它们的零值,并返回一个指向该类型的指针。

  1. 初始化 map: 使用 new 初始化 map 会得到一个指向 map 类型的指针,其值为 nil,表示一个空的 map。需要注意的是,尽管 map 被初始化为 nil,但是仍然可以进行操作,例如对其进行赋值、添加键值对等操作。然而,在使用 map 前,需要确保对其进行初始化,通常使用 make 来创建非空的 map

    示例:

    goCopy codem := new(map[string]int)
    fmt.Println(m) // 输出: &map[]
    
    • 1
    • 2
  2. 初始化 slice: 使用 new 初始化 slice 会得到一个指向 slice 类型的指针,其值为 nil,表示一个空的 slice。与 map 类似,尽管 slice 被初始化为 nil,但是仍然可以进行操作,例如对其进行赋值、追加元素等操作。在使用 slice 前,同样需要对其进行初始化,通常使用 make 来创建非空的 slice

    示例:

    goCopy codes := new([]int)
    fmt.Println(s) // 输出: &[]
    
    • 1
    • 2
  3. 初始化 channel: 使用 new 初始化 channel 会得到一个指向 channel 类型的指针,其值为 nil,表示一个未初始化的 channel。这样的 channel 无法直接使用,需要使用 make 创建一个具体的通道并分配相应的缓冲区大小后才能使用。

    示例:

    goCopy codech := new(chan int)
    fmt.Println(ch) // 输出: <nil>
    
    • 1
    • 2

综上所述,虽然可以使用 new 初始化 mapslicechannel,但得到的是一个指向对应类型的指针,其值为 nil。如果需要创建一个非空的 mapslicechannel,通常建议使用 make 函数进行初始化,并分配相应的内存和缓冲区。

(3)2 个 nil 可能不相等吗?

可能不等。interface在运行时绑定值,只有值为nil接口值才为nil,但是与指针的nil不相等。举个例子:

var p *int = nil
var i interface{} = nil
if(p == i){
	fmt.Println("Equal")
}
  • 1
  • 2
  • 3
  • 4
  • 5

两者并不相同。总结:两个nil只有在类型相同时才相等

六、grpc相关的面试题

(1)grpc是什么

gRPC(gRPC Remote Procedure Call)是一种高性能、通用的开源远程过程调用(RPC)框架,由Google开发并开源。它基于HTTP/2协议,并使用Protocol Buffers作为默认的序列化机制。

gRPC旨在简化分布式应用程序之间的通信,使得客户端和服务器可以像调用本地方法一样调用远程服务。它支持多种编程语言(如Go、Java、C++、Python等),允许开发者使用定义服务接口的方式来描述请求和响应的数据结构,然后自动生成相应的客户端和服务器端代码。

gRPC具有以下特点和优势:

  1. 高性能:gRPC基于HTTP/2协议,采用了二进制传输和多路复用等技术,具有较低的延迟和高吞吐量,适用于高性能的分布式系统。
  2. 跨平台和多语言支持:gRPC支持多种编程语言,使得不同语言的应用程序可以相互通信,方便实现跨平台和跨语言的分布式应用。
  3. 自动生成代码:通过使用Protocol Buffers定义服务接口和消息类型,gRPC可以自动生成客户端和服务器端的代码,减少了手动编写繁琐的网络通信代码的工作量。
  4. 支持多种通信模式:gRPC支持四种常见的通信模式,包括单一请求-单一响应、单一请求-流式响应、流式请求-单一响应以及流式请求-流式响应,使得开发者可以根据实际需求选择适合的通信方式。
  5. 强大的拦截器和中间件支持:gRPC提供了丰富的拦截器和中间件支持,可以在请求和响应的处理过程中进行拦截和处理,方便实现认证、授权、日志记录等功能。
  6. 可扩展性:通过使用Protocol Buffers,可以定义复杂的数据结构和服务接口,并支持版本化、演进和后向兼容等扩展性方面的需求。

总之,gRPC是一种功能强大的远程过程调用框架,提供了高性能、跨平台和多语言支持,适用于构建分布式系统中的服务间通信。它简化了开发者的工作,提供了简洁、高效和可扩展的解决方案。

七、goroutine相关的面试题

(1)进程被kill,如何保证所有goroutine顺利退出

当进程被kill时,操作系统会终止进程并清理相关资源,包括所有的goroutine。在这种情况下,无法直接保证所有的goroutine能够顺利退出。不过,可以采取一些措施来优雅地退出goroutine并确保资源的正确释放。

  1. 通过信号通知:在进程被kill之前,可以通过操作系统的信号机制向进程发送特定的信号(如SIGINT、SIGTERM),在信号处理函数中做一些清理工作并通知goroutine退出。可以使用Go的os/signal包来捕获信号并执行相应的处理逻辑。
  2. 使用context来控制goroutine:在启动goroutine时,传递一个context.Context对象给它,可以通过该对象来控制goroutine的生命周期。在进程即将退出时,可以调用cancel函数取消所有相关的context,从而通知goroutine退出。
  3. 使用sync.WaitGroup等待goroutine退出:在主goroutine中,使用sync.WaitGroup来等待所有的goroutine完成。在goroutine退出时,通过Done方法减少WaitGroup的计数器,主goroutine可以通过Wait方法等待所有的goroutine退出。
  4. 使用通道(Channel)来通知退出:在goroutine中使用一个退出通道来接收退出信号,当需要退出时,向通道发送退出信号。其他的goroutine可以监听该通道,收到退出信号后进行清理工作并退出。

需要注意的是,对于一些无法中断或阻塞的操作,比如网络请求或文件IO等,可能需要通过设置超时或取消的机制来确保它们能够及时退出。

综上所述,保证所有goroutine顺利退出的方法可以结合使用信号通知、使用context控制、使用sync.WaitGroup等待、使用退出通道等多种手段。具体的实现方式取决于应用的需求和场景。

(1.1)通过信号通知

package main

import (
	"context"
	"fmt"
	"os"
	"os/signal"
	"sync"
	"syscall"
	"time"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	wg := sync.WaitGroup{}

	// 启动多个goroutine
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			for {
				select {
				case <-ctx.Done():
					fmt.Printf("Goroutine %d: Exiting\n", id)
					return
				default:
					fmt.Printf("Goroutine %d: Running\n", id)
					time.Sleep(time.Second)
				}
			}
		}(i)
	}

	// 捕获终止信号
	signalCh := make(chan os.Signal, 1)
	signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM)

	// 等待终止信号
	<-signalCh
	fmt.Println("Received termination signal. Cleaning up...")

	// 发送取消信号,通知所有goroutine退出
	cancel()

	// 等待所有goroutine退出
	wg.Wait()

	fmt.Println("All goroutines have exited. Exiting main...")
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

(1.2)使用context控制:

goCopy codepackage main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	wg := sync.WaitGroup{}

	// 启动多个goroutine
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			for {
				select {
				case <-ctx.Done():
					fmt.Printf("Goroutine %d: Exiting\n", id)
					return
				default:
					fmt.Printf("Goroutine %d: Running\n", id)
					time.Sleep(time.Second)
				}
			}
		}(i)
	}

	// 等待一段时间后,取消所有goroutine
	time.Sleep(5 * time.Second)
	cancel()

	// 等待所有goroutine退出
	wg.Wait()

	fmt.Println("All goroutines have exited. Exiting main...")
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

(2)goroutine什么情况会发生内存泄漏?如何避免。

在Go中内存泄露分为暂时性内存泄露和永久性内存泄露。

暂时性内存泄露

  • 获取长字符串中的一段导致长字符串未释放
  • 获取长slice中的一段导致长slice未释放
  • 在长slice新建slice导致泄漏

string相比切片少了一个容量的cap字段,可以把string当成一个只读的切片类型。获取长string或者切片中的一段内容,由于新生成的对象和老的string或者切片共用一个内存空间,会导致老的string和切片资源暂时得不到释放,造成短暂的内存泄漏

永久性内存泄露

  • goroutine永久阻塞而导致泄漏
  • time.Ticker未关闭导致泄漏
  • 不正确使用Finalizer(Go版本的析构函数)导致泄漏

在Go中,goroutine会在其执行完成后自动被垃圾回收,因此不会出现传统意义上的内存泄漏。然而,有一些情况可能导致goroutine无法正常退出,从而导致资源泄漏或无法释放的问题。以下是一些可能导致goroutine泄漏的情况以及如何避免它们:

  1. 未关闭的通道(channel):如果一个goroutine向一个通道发送数据,而没有其他goroutine接收该数据,该goroutine将会被阻塞并无法退出。为了避免这种情况,确保在不需要继续发送数据时,正确地关闭通道。
  2. 循环引用:如果一个goroutine持有对其他对象的引用,而这些对象又持有对该goroutine的引用,就会形成循环引用。这可能导致垃圾回收器无法回收这些对象,从而导致内存泄漏。为了避免循环引用,确保在不再需要时,及时解除对对象的引用。
  3. 资源未释放:如果goroutine在完成任务后没有正确释放所使用的资源(如打开的文件、数据库连接等),就可能导致资源泄漏。确保在不再需要资源时,及时进行关闭、释放或销毁。
  4. 无限循环或阻塞:如果goroutine进入无限循环或阻塞状态,它将无法正常退出并释放相关资源。确保goroutine的执行逻辑能够合理终止或定时退出,避免陷入无限循环或永久阻塞的情况。
  5. 忘记等待goroutine完成:如果主goroutine在退出前没有等待其他goroutine完成,可能会导致这些goroutine无法完成任务或资源清理。使用sync.WaitGroup等机制来等待所有需要等待的goroutine完成。

总之,避免goroutine的内存泄漏主要需要确保通道的正确关闭、避免循环引用、及时释放资源、避免无限循环或阻塞,以及适当等待其他goroutine完成。通过合理的设计和资源管理,可以避免大多数goroutine泄漏和资源泄漏问题。

八、defer相关面试题

(1)了解Go的defer语句吗?它在runtime中是如何实现的?

defer语句是Go语言中的一种特性。它用于在函数执行完毕后或发生panic时,按照后进先出(LIFO)的顺序执行一系列预定的函数调用。defer语句非常实用,可以确保某些清理或收尾工作无论函数是如何退出(正常返回、panic或运行时错误)都会被执行。

在Go的运行时中,defer语句通过类似堆栈的数据结构实现。当遇到defer语句时,函数调用和其参数会被推入一个defer栈的顶部。栈会保持defer语句执行的顺序,因此最后被推入栈的defer语句会最先执行,而最先被推入栈的会最后执行,等到包围的函数返回时进行逆序执行。

当包围的函数执行完毕,无论是正常返回还是panic,Go运行时会开始依次执行defer栈中的函数调用。即使发生了panicdefer语句也会被执行,这样在panic向上传播之前可以确保关键的清理或资源释放操作。

下面是一个示例,演示了defer语句的行为:

package main

import "fmt"

func cleanup() {
    fmt.Println("执行清理操作...")
}

func main() {
    fmt.Println("开始主函数")

    defer fmt.Println("延迟执行语句 1")
    defer fmt.Println("延迟执行语句 2")
    defer cleanup()

    panic("发生了 panic")

    fmt.Println("结束主函数") // 这行代码永远不会被执行
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

开始主函数

执行清理操作…

延迟执行语句 2
延迟执行语句 1

panic: 发生了 panic

goroutine 1 [running]:
main.main()
/路径/文件.go:13 +0x9a
exit status 2

九、异常相关面试题

(1)golang可以自定义异常吗

在go里定义错误异常的方式有这么两种,但都需要你的返回值是error类型的:

  1. 第一种方式是使用golang标准库包errors 来定义错误。使用方法很简单,只需要 return errors.New(“错误信息”) 。 这样就是一个最简单的错误返回。
  2. 第二种方式是借用struct结构体,创建一个struct的Error()方法,注意这个方法名是Error,不然会出现找不到Error方法。
package main

import (
    "errors"
    "fmt"
)

type equalError struct {
    Num int
}

//方法名字是Error()
func (e equalError) Error() string {
    return fmt.Sprintf("当前数字是 %d ,大于10", e.Num)
}

//使用errors.New简单生成
func Equal(n int) (int, error) {
    if n > 10 {
        return -1, errors.New("大于10") //生成一个简单的 error 类型
    }
    return n, nil
}

func DiyEqual(n int) (int, error) {
    if n > 10 {
        return -1, equalError{Num: n} // 会调用equalError的Error方法
    }
    return n, nil
}

func main() {
    //使用errors.New生成error对象
    if result, err := Equal(20); err != nil {
        fmt.Println("错误:", err)
    } else {
        fmt.Println("结果:", result)
    }

    //不适用erros,自定义错误方式.
    if result, err := DiyEqual(20); err != nil {
        fmt.Println("错误:", err)
    } else {
        fmt.Println("结果:", result)
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

十、map相关面试题

在Go语言的面试中,关于map的问题也是很常见的。以下是一些可能出现的面试题:

  1. map的底层数据结构是什么?map如何实现快速查找元素?
  2. map的key可以是哪些类型?有没有限制?
  3. map的value可以是哪些类型?有没有限制?
  4. map是否支持并发读写?如何安全地在多个goroutine中使用map?
  5. map的零值是什么?如何判断一个map是否为nil?
  6. 如何初始化一个空的map?有哪些创建map的方法?
  7. 如何向map中插入键值对?如何从map中删除键值对?
  8. 如何通过key从map中查找元素?如何判断一个key是否存在于map中?
  9. map的长度是指什么?如何获取map的长度?
  10. map是否支持负索引?为什么?
  11. map在并发编程中是否安全?如何避免并发访问map时的竞态条件?
  12. map的遍历方式有哪些?有没有固定的遍历顺序?
  13. map的迭代顺序是否是随机的?
  14. 如何使用map来实现set和计数器?
  15. map是否可以作为函数的参数和返回值?

(1)go中map的底层实现

在Go中,map的底层实现是一个哈希表(hash table),也称为散列表。哈希表是一种用于存储键值对的数据结构,它通过将键映射到哈希值,然后将哈希值映射到数组的索引来快速访问和查找数据。

具体来说,Go中的map是由一个hmap结构体表示的,它定义在runtime/map.go中,部分结构如下:

goCopy codetype hmap struct {
	count     int        // 当前存储的键值对数量
	flags     uint8      // 状态标志,例如是否是扩容中
	B         uint8      // 桶的大小的移位数,用于计算桶的数量,比如2^B
	noverflow uint16     // 溢出桶的数量,用于解决哈希冲突
	hash0     uint32     // 哈希种子,用于计算哈希值
	buckets   unsafe.Pointer // 指向桶数组的指针
	oldbuckets unsafe.Pointer // 指向扩容前的桶数组的指针,用于迁移数据
}

type bmap struct {
	topbits  [8]uint8 // 桶的最高8位的哈希值,用于快速定位
	keys     [8]keytype // 键数组
	values   [8]valuetype // 值数组
	overflow uintptr // 指向溢出桶的指针,用于解决哈希冲突
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

Go的map使用了哈希表的经典实现,具有以下特点:

  1. 哈希冲突解决:当两个不同的键映射到相同的哈希值时,Go的map使用链地址法(separate chaining)来解决哈希冲突。即在哈希表中的每个桶里面存储一个链表,哈希值相同的键值对会被放到同一个桶的链表中。
  2. 动态扩容:随着键值对的增加,当哈希表的负载因子(键值对数量与桶数量之比)超过一定阈值时,Go的map会进行动态扩容,重新分配更大的桶数组,以减少哈希冲突,保持高效的查找性能。
  3. 哈希种子:为了防止哈希攻击,Go的map使用随机种子(哈希种子)来计算哈希值。每次创建map时,都会生成一个随机的哈希种子,并保存在map的hash0字段中。
  4. 并发安全:在读取和写入map时,必须保证并发安全。在多个goroutine同时读写map时,需要使用适当的同步机制,例如互斥锁(sync.Mutex)来保护map的并发访问。

总之,Go中的map是一个高效、动态扩容的哈希表实现,可以在常数时间内进行插入、查找和删除操作。但是需要注意在并发场景下的正确使用,避免因为并发访问而引发竞态条件。

(2)map的key可以是哪些类型?有没有限制?

在Go中,map的key可以是以下几种类型:

  1. 所有支持相等运算符(==)的数据类型,例如:整数类型(int、int8、int16、int32、int64)、浮点数类型(float32、float64)、字符串类型(string)、布尔类型(bool)等。
  2. 数组类型、结构体类型和指针类型,只要它们的元素或字段的类型是可比较的,也就是它们本身支持相等运算符。
  3. 接口类型,只要它们的动态类型是可比较的。

但是,有一些类型是不允许作为map的key的:

  1. 切片类型(slice):因为切片是动态长度的,不能通过相等运算符进行比较。
  2. 函数类型:因为函数是不可比较的。
  3. 包含切片或函数的结构体类型:由于切片和函数不可比较,如果结构体包含切片或函数字段,则也不能作为map的key。

值得注意的是,使用结构体类型作为map的key时,只有当结构体的所有字段都是可比较的才可以。如果结构体中包含不可比较的字段(如切片或函数),那么该结构体也不能作为map的key。

十一、基础面试题

(1)go和Java有哪些区别?

Go和Java是两种不同的编程语言,它们在许多方面有着显著的区别。以下是Go和Java之间的一些主要区别:

  1. 语言设计和语法:Go的语法相对较简洁和紧凑,强调代码的可读性和易于编写。它具有C风格的语法,使用显式类型声明和关键字来定义变量和函数。Java语法更为冗长,使用较多的关键字和语法结构,支持面向对象编程范式。
  2. 并发和并行:Go在语言层面提供了轻量级的协程(goroutine)和通道(channel)机制,用于实现并发编程。它内置了高效的并发原语,并提供了简洁的方式来编写并发代码。相比之下,Java使用线程和锁来实现并发,需要开发人员手动管理线程和同步机制。
  3. 内存管理:Go使用垃圾回收器(Garbage Collector)自动管理内存,开发人员不需要手动分配和释放内存。Java也有垃圾回收器,但它的内存管理机制更为复杂,包括堆内存、栈内存和手动的内存释放(如finalize()方法)。
  4. 依赖管理:Go使用Go Modules进行依赖管理,允许开发人员声明和管理项目的依赖关系,实现版本控制和构建复现性。Java使用Maven或Gradle等构建工具来管理依赖项,通过配置文件(如pom.xml)指定项目的依赖关系。
  5. 性能:由于Go的语言设计和运行时环境的特性,它在许多情况下表现出较高的性能和低延迟。相比之下,Java具有更广泛的生态系统和优化工具,可以在不同的场景中实现高性能。
  6. 领域应用:Go主要用于构建网络服务和分布式系统,如Web服务器、微服务和容器编排。它在处理并发、高性能和可伸缩性方面表现出色。Java则广泛应用于企业级应用程序开发,包括桌面应用程序、后端服务器和大规模企业应用。

Reference

  1. https://zhuanlan.zhihu.com/p/471490292
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/514429
推荐阅读
相关标签
  

闽ICP备14008679号