背景
大多数情况下,面试时候写代码都会直接上 leetcode, 但是少数情况下,会让你写一些非算法的代码,这个几率可能占到10%-20%。这些题目一般都和算法思想无关,很多都是固定的模板写法,如果没有准备很容易直接凉凉,而且凉得很难看很憋屈。 尤其是 golang 并发编程部分,包括 go routine, go channel , waitgroup, mutex, context面试官非常喜欢考。 为此特意写一贴,记录那些反复,多次被问到的,或者极其有可能会被问到的 golang 非算法代码题目。
- 单例模式
- 两个协程交替打印输出
- 写一个死锁并且分析
- go routine 向外通信
- 写一个闭包
- 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)
}
这个例子满足了上面的四个条件
- 对于非缓冲的 go channel,必须是一个go routine 读,一个 go rouitne 写的,两者存在竞争。这里只有一个 主协程,但在 channel 看来 是有两个的一个读,一个写。
- 读的 协程不会放弃 go channel, 会一直占用
- 读的 协程 占用的 go channel 没有被别的协程剥夺,废话这里就一个主协程,拿什么去剥夺
- 循环等待,主协程在等待 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