盘点那些 golang 面试非算法代码题(持续更新中)

1,548 阅读6分钟

背景

大多数情况下,面试时候写代码都会直接上 leetcode, 但是少数情况下,会让你写一些非算法的代码,这个几率可能占到10%-20%。这些题目一般都和算法思想无关,很多都是固定的模板写法,如果没有准备很容易直接凉凉,而且凉得很难看很憋屈。 尤其是 golang 并发编程部分,包括 go routine, go channel , waitgroup, mutex, context面试官非常喜欢考。 为此特意写一贴,记录那些反复,多次被问到的,或者极其有可能会被问到的 golang 非算法代码题目。

  1. 单例模式
  2. 两个协程交替打印输出
  3. 写一个死锁并且分析
  4. go routine 向外通信
  5. 写一个闭包
  6. context 主动控制 go routine 执行

1.单例模式

一般面试官会问你了解设计模式吗? 说一说你知道的,大多数人顺口一说单例模式。单例模式比较简单,面试官十有八九都会让你现场写一个出来。

懒汉式写法(调用的时候才去判断是否创建)

package main

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

type singleton struct {
   current time.Time
}

var instance *singleton
var lock sync.Mutex

func GetInstance() *singleton {
   lock.Lock()

   if instance == nil{
      instance = &singleton{
         current: time.Now(),
      }
   }

   lock.Unlock()
   return instance
}

func main()  {
   
   s1:=GetInstance()
   fmt.Println(s1.current)

   time.Sleep(time.Second)
   
   s2:=GetInstance()
   fmt.Println(s2.current)

}

饿汉式(一开始就创建好对象)

package main

import (
   "fmt"
   "time"
)

type  singleton struct {
   current time.Time
}

var instance = &singleton{current:time.Now()}

func getInstance()  *singleton {
   return  instance;
}

func main()  {

   s1:=getInstance();
   fmt.Println(s1.current)

   time.Sleep(time.Second)

   s2:=getInstance();
   fmt.Println(s2.current)

}

2.两个线程/协程交替打印输出

这个题目我遇到两次了,也凉过两次了,Java和 golang 的面试官都喜欢这么考。要是提前不准备,面试基本凉凉。

package main

import (
   "fmt"
   "sync"
)


func main() {

   wg := sync.WaitGroup{};
   c1 := make(chan int,1);
   c2 := make(chan int);
   wg.Add(2);

   c1 <-1;
   go func() {
      defer  wg.Done()
      for i:=0; i<10 ; i++  {
         <- c1
         fmt.Println("A")
         c2<-1
      }
   }()


   go func() {
      defer  wg.Done()
      for i:=0; i<10 ; i++  {
         <-c2
         fmt.Println("B")
         c1<-1
      }
   }()



   
   wg.Wait()
}

3.让你写一个死锁

面试官通常会这么问, 你知道死锁吗? 你会吭哧吭哧把死锁的四个条件背下来: 1.资源不够,资源被竞争访问 2. 获得资源的线程/协程 在获取CPU执行之前不会主动释放资源 3.获得资源的线程/ 协程 的资源不会被强行剥夺 4. 循环等待 。

好的接下来,面试官很有可能让你写一个死锁。golang 写一个死锁并不难,甚至是相当简单,难的地方在于,你如何分析出这种死锁满足了上面的4个条件,以及如何通过破坏上面的四个条件 来解决死锁。

package main

import "fmt"

func main()  {
   c1:=make(chan int)
   fmt.Println(<-c1)
}

这个例子满足了上面的四个条件

  1. 对于非缓冲的 go channel,必须是一个go routine 读,一个 go rouitne 写的,两者存在竞争。这里只有一个 主协程,但在 channel 看来 是有两个的一个读,一个写。
  2. 读的 协程不会放弃 go channel, 会一直占用
  3. 读的 协程 占用的 go channel 没有被别的协程剥夺,废话这里就一个主协程,拿什么去剥夺
  4. 循环等待,主协程在等待 go channel 里面有东西可以读,而 go channel 等待别人来写,但是就一个主协程,没有人来写。

解决方法

解决方法也很简单,就是先启动一个 go routine 去读 channel, 主携程去写chan

package main

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

func main() {
   c1 := make(chan int)
   wg := sync.WaitGroup{}
   wg.Add(1)
   go func() {
      defer wg.Done()
      time.Sleep(time.Second)
      fmt.Println(<-c1)
   }()

   c1 <- 1
   wg.Wait()
}

4.如何从 go routine 中获取返回值

go routine 依靠 channel 向外进行数据流通, 面试官很有可能让你写一下如何从 go routine 中获取返回值。这道题目要是对 go channel, go routine, waitgroup 用法不熟悉,大概率直接凉凉。

func main() {
   c := make(chan int, 1)
   wg := sync.WaitGroup{}
   wg.Add(1)
   go func() {
      defer wg.Done()
      c <- 1
   }()
   wg.Wait()
   fmt.Println("c1 :", <-c)
}

5. 写一个闭包

由于golang 并不是一门面向对象的编程语言,而是面向过程,面向过程的语言通常有一个闭包的特性。 面试官很有可能会问你,什么是闭包。 你会吭哧吭哧背:闭包简单来说就是让一个函数拥有保存变量的能力。于是面试官让你手写一个闭包的例子。很多 golang 的同学之前是学 Java 的,遇到这种函数式编程,极其不适应,如果没有准备好一个例子,大概率凉凉。

package main

func plus() func() {
   sum:=0
   return func() {
      println(sum)
      sum+=1;
   }
}

func main()  {
   plusFunc:=plus();
   plusFunc()
   plusFunc()
   plusFunc()
   plusFunc()
}

6. Context 用法

contex 通常会和 go routine 一起考。 还是那个套路,问你 go 为什么快, 回答 go routine 协程, 问你协程为什么快,因为协程的需要保存的上下文信息更少,因为协程无法主动释放资源, 重点来了:既然无法主动释放资源,那么如何控制 go routine 的执行呢,要是 go routine 里面代码遇到死循环,永远出不来了,你怎么监控它,让它退出。这时候你会说用 context 里面的定时器,或者直接手动关闭这个 go routine。

package main

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

func watch(ctx context.Context, contexName string)  {
   go func() {
      for {
         select {
         case <-ctx.Done():
            fmt.Println(contexName, " stop ")
            return
         default:
            time.Sleep(100*time.Millisecond)// 每100ms监测一次
         }
      }

   }()
}

func main() {

    rootCtx:=context.Background()

    ctx1,_ :=context.WithDeadline(rootCtx, time.Now().Add(time.Second))// 1秒后结束运行,到达某个时间点结束
    watch(ctx1,"WithDealine context")

    ctx2,_:=context.WithTimeout(rootCtx, 2*time.Second)// 只运行2秒, 从开始运行算起
    watch(ctx2,"WithTimeout context")

    ctx3,cancel:= context.WithCancel(rootCtx)// 可以手动结束的 context
    watch(ctx3,"WithCancel context")

    time.Sleep(3*time.Second)
    cancel()//3秒后手动结束运行

    time.Sleep(time.Second)
}

继续更新哈

7. 实现 slice 的深拷贝 和 浅拷贝

slice 也是 golang 面试里面的高频考点了。slice 结构体里面有3个部分: slice 起始地址的指针,容量, slice里面有效元素的个数。 由于有个指针, 所以很自然的涉及到了 深拷贝 和 浅拷贝。深拷贝就是 彻彻底底的 创建了一个新的 slice, 新的 slice 和老的 slice 完全是两份东西,地址不一样。 而 浅拷贝 就是给老的 slice 新起了一个名字,改变新老 slice 的任意一方,另一方也改变。除了调用 copy 函数,其他操作都是浅复制。 面试官有可能让你实现以下深拷贝和浅拷贝,代码如下:

package main

import "fmt"

func main() {

   a:=[]int{0,1,2}

   // 深拷贝
   var b  =make([]int, 3);

   copy(b, a)
   b[0]=-1;
   fmt.Println(a)
   fmt.Println(b)

   // 浅拷贝
   c:=a ;
   c[0]=-1;
   fmt.Println(a)
   fmt.Println(c)


}

8. select 的用法

select 多个 channel, 直接打印。由于 channel没有 缓冲, 所以我特意休眠了一秒,为得是先用 select 进行读操作, 避免死锁。

package main

import (
   "fmt"
   "time"
)

func main() {
   ch1 := make(chan int)
   ch2 := make(chan int)

   go func() {
      ch1 <- 1
   }()

   go func() {
      ch2 <- 2
   }()

   time.Sleep(time.Second)

   select {
   case num := <-ch1:
      fmt.Println("read from ch1:", num)
   case num := <-ch2:
      fmt.Println("read from ch2:", num)
   }
}

预期输出并不是一个确定值, 有可能是从 ch1 中读到了:

read from ch1: 1

也有可能是从 ch2 中读到了

read from ch1: 1

9. pannic 和 recover

recover 类似于 java 中的 try cat 语句, 能捕获到 panic

 package main

import "fmt"

func divide(a, b int) int {

   defer func() {
      err := recover()
      if err != nil {
         fmt.Println("err = ", err)
      }
   }()

   if b == 0 {
      panic("divided by zero")
   } else {
      return a / b
   }

}

func main() {
   _ = divide(2, 0)
}
 

运行结果:

err =  divided by zero