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周赛