GO面试精选题目,学会这些就能应付80%的面试啦


0.写在前面

有的人觉得go面试很难,其实相对于java来说,go的内容已经很少了。作为go后端开发者,go基础面试也就那几个问题,面试的时候主要还是要把后端的(跟语言无关的)知识打牢,比如数据库、分布式、缓存、编程风格等等。

本文主要解决的是go基础知识部分,学会了这十多个问题,相信能应付绝大部分的面试。

1.并发编程

问:go中是如何实现并发的

答:go是通过goroutine实现并发的。goroutine在go语言中叫做协程,它底层其实就是一个结构体,它是go程序的调度单位,纯语言层面实现的。goroutine是用户态线程,操作系统无法感知goruntine的存在,它的创建、调度、销毁完全由go执行器负责。下面是一个简单的示例。

func main() {
   ticket := 10
   l := sync.Mutex{}
   wg := sync.WaitGroup{}
   wg.Add(ticket)
   for i := 0; i < ticket; i++ {
      go func() {
         l.Lock()
         defer l.Unlock()
         if ticket <= 0 {
            return
         }
         fmt.Printf("goroutine1 get ticket:%d\n", ticket)
         wg.Done()
         ticket--
      }()
   }
   for i := 0; i < ticket; i++ {
      go func() {
         l.Lock()
         defer l.Unlock()
         if ticket <= 0 {
            return
         }
         fmt.Printf("goroutine2 get ticket:%d\n", ticket)
         wg.Done()
         ticket--
      }()
   }
   wg.Wait()
   fmt.Println("ticket empty")
}

结果

goroutine1 get ticket:10
goroutine1 get ticket:9
goroutine1 get ticket:8
goroutine1 get ticket:7
goroutine1 get ticket:6
goroutine2 get ticket:5
goroutine1 get ticket:4
goroutine2 get ticket:3
goroutine2 get ticket:2
goroutine2 get ticket:1
ticket empty

2.defer

问:go defer是什么?有什么作用

答:go中的defer表示延迟执行。即在函数返回之前执行。一般用于关闭链接、句柄、清理、释放锁、gorountine中捕获异常等工作。比如打开文件

func main() {
   f, err := os.Open("main.go")
   if err != nil {
      panic(err)
   }
   defer f.Close()
   
   // TODO 其他操作
}

打开文件的时候容易忘记关闭,一般在打开之后,立马写一个defer语句关闭文件,defer这行代码会在函数返回之前执行。

如果一个函数中定义了多个defer,它遵循先定义后执行的原则。即所有defer语句会放在一个栈中。

需要注意的是,在defer语句中更改return的返回值会生效吗?有可能生效、有可能不生效。详情在作者另外一篇文章中专门介绍Go defer中改变return的值会生效吗?

3.map

问:go map底层实现逻辑能大概讲一下吗?以及它是怎么扩容的?

答:go的map底层使用数组实现,并且用拉链法处理hash冲突。它的扩容有两种:增量扩容和等量扩容。

  • 当容量比例超过负载因子6.5就会发生增量扩容。计算公式:元素总量/总容量>=6.5,扩容成原来的2倍。
  • 当溢出桶的数量超过总容量,发生等量扩容。

go map内容挺多的,如果想了解更多,可查看作者其他go map相关文章

4.slice

问:slice底层结构了解吗?扩容机制是什么?

答:slice底层定义是一个结构体,有3个属性

type slice struct {
   array unsafe.Pointer
   len   int
   cap   int
}

扩容机制:

  • 当新容量>2*旧容量,直接使用新容量
  • 旧容量<256,就容量直接翻倍
  • 旧容量>=256,新容量=旧容量+(旧容量+2563)/4=旧容量1.25+192,依次循环,直到新容量满足条件(大于需要的容量)即可。

详情可查看作者之前写的文章

问:在函数内部修改入参slice的元素值,函数返回后会有影响吗?

答:可能会、可能不会,看是否发生了扩容。这是一个大坑,非常容易出错,一般不建议这么做。在函数内部能修改入参slice吗?不一定文章中有详细介绍。

问:如何快速移除切片中的某个元素

答:可以使用append

func main() {
  s := []int{1, 2, 3, 4, 5}
  deleteIdx := 2
  s = append(s[:deleteIdx], s[deleteIdx+1:]...)
  fmt.Println(s) // [1 2 4 5]
>}

5.recovery

问:recovery一般何时使用?如何使用?

答:recoery是指当程序发生panic的时候,使用recovery能捕获panic,防止程序崩溃。一般在goroutine中使用

func main() {
   wg := sync.WaitGroup{}
   wg.Add(1)
   go func() {
      defer func() {
         if err := recover(); err != nil {
            fmt.Printf("recovery panic:%v\n", err)
         }
         wg.Done()
      }()

      testPainc()
   }()
   wg.Wait()
   fmt.Println("end")
}

func testPainc() {
   panic("this is a test panic")
}

结果:

recovery panic:this is a test panic
end

6.原子操作和锁

问:go中的原子操作和锁分别是什么?有什么区别?

答:原子操作和锁是并发编程中常用的两种同步机制,他们的区别如下:

  • 作用范围
    • 原子操作(Atomic Operations):原子操作是一种基本的操作,可以在单个指令级别上执行,保证操作的原子性。原子操作通常用于对共享变量进行读取、写入或修改等操作,以确保操作的完整性。
    • 锁(Lock):锁是一种更高级别的同步机制,用于保护临界区(Critical Section)的访问。锁可以用于限制对共享资源的并发访问,以确保线程安全。
  • 使用方式
    • 原子操作:原子操作是通过硬件指令或特定的原子操作函数来实现的,可以直接应用于变量或内存位置,而无需额外的代码。
    • 锁:锁是通过编程语言提供的锁机制来实现的,需要显式地使用锁的相关方法或语句来保护临界区的访问。
  • 粒度
    • 原子操作:原子操作通常是针对单个变量或内存位置的操作,可以在非常细粒度的层面上实现同步。
    • 锁:锁通常是针对一段代码或一组操作的访问进行同步,可以控制更大粒度的临界区。
  • 性能开销
    • 原子操作:原子操作通常具有较低的性能开销,因为它们是在硬件级别上实现的,无需额外的同步机制。
    • 锁:锁通常具有较高的性能开销,因为它们需要进行上下文切换和线程同步等操作。

综上所述,原子操作和锁是两种不同的同步机制,用于处理并发编程中的同步问题。原子操作适用于对单个变量的读写操作,具有较低的性能开销。而锁适用于对一段代码或一组操作的访问进行同步,具有更高的性能开销。选择使用原子操作还是锁取决于具体的场景和需求。

需要注意的是,原子操作通常用于对共享变量进行简单的读写操作,而锁更适用于对临界区的访问进行复杂的操作和保护。在设计并发程序时,需要根据具体的需求和性能要求来选择合适的同步机制。

7.channel

问:go中channel作用是什么?底层是如何实现的?

答:channel是goroutine之间传递信息的一种机制。go的理念是通过通信实现共享内存。多个goroutine并发的时候,之间通过channel实现数据传递。channel有2个基本操作:

  • v, ok <- ch; 从channel中读取数据
  • ch <- 1; 向channel中写入数据

底层是通过锁和环形数组实现的一个双端队列,详情可查go chan 设计与实现

7.select

问:select有什么作用

答:select语句是Go语言中用于处理通道操作的一种机制。它可以同时监听多个通道的读写操作,并在其中任意一个通道就绪时执行相应的操作。

select一般用于超时控制,例如

func main() {
   ch := make(chan int)
   go func(chan int) {
      testRPC(ch)
   }(ch)

   select {
   case v, ok := <-ch:
      if ok {
         fmt.Printf("rpc get value:%d\n", v)
      }
   case <-time.After(time.Second * 2):
      fmt.Println("timeout")
   }
}

// 测试rpc超时
func testRPC(ch chan<- int) {
   time.Sleep(time.Second * 3)
   ch <- 1
}

结果:timeout

如果将testRPC执行时间改成1s,得到的结果是rpc get value:1

select的底层实现原理可查看GO-select 的实现原理

8.并发

问:多个goroutine访问同一个元素如何保证并发安全?

答:有多种方法

  • 使用互斥锁(Mutex):通过使用互斥锁来保护共享资源的访问,一次只允许一个goroutine访问共享资源,从而避免竞争条件。
  • 使用原子操作(Atomic Operations):对于简单的读写操作,可以使用原子操作来保证操作的原子性,避免竞争条件。
  • 使用通道(Channel):通过使用通道来进行goroutine之间的通信和同步,避免共享资源的直接访问。
  • 使用同步机制:使用同步机制如等待组(WaitGroup)、条件变量(Cond)等来协调多个goroutine的执行顺序和状态。
  • 信号量。

9.垃圾回收

问:go的垃圾回收机制了解吗?

答:了解一些。go通过三色标记法实现垃圾回收的。

  • 首先把所有的对象都放到白色的集合中
  • 从根节点开始遍历对象,遍历到的白色对象从白色集合中放到灰色集合中
  • 遍历灰色集合中的对象,把灰色对象引用的白色集合的对象放入到灰色集合中,同时把遍历过的灰色集合中的对象放到黑色的集合中
  • 循环上一步,直到灰色集合中没有对象
  • 灰色集合中没有对象了,白色集合中的对象就是不可达对象,也就是垃圾,进行回收

详情可查看go 垃圾回收那些事儿

10.总结

本文总结了go相关的9个面试高频问题,认真搞懂每一个问题,轻松应付go面试。

另外,作为后端开发,手撕算法题肯定是面试过程中不可避免的,作者最近参加了华为面试,总结了一下经验华为GO一面,可供学习。

作者最近半年以来,每周做leetcode周赛题目,并且每天也保持一道或者两道算法题,感兴趣的同学可以加个微信16602756241,拉群一起刷leetcode。一起坚持做leetcode周赛

11.参考

[1]Go defer中改变return的值会生效吗?

[2]go map那些事儿

[3]go 可排序的map

[4]go map 面试十连问,你扛得住吗?

[5]https://github.com/ZBIGBEAR/sort_map

[6]go chan 设计与实现

[7]GO-select 的实现原理

[8]go 垃圾回收那些事儿

[9]华为GO一面

[10]精选Go高频面试题和答案汇总,阅读破万,收藏230+

[11]一起坚持做leetcode周赛


文章作者: Alex
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Alex !
  目录