知识体系之Go面试题
go八股文
目录
7. Goroutine什么时候会发生阻塞?阻塞的话调度器会怎么做?
8. 在GMP模型中Goroutine有几种状态,线程几种状态
9. 若干线程中,有个线程OOM会怎么样?Goroutine发生OOM呢?怎么排查呢?
10. defer可以捕获到子Goroutine的panic吗?
11. 开发用过gin框架么?参数校验怎么做的?中间件middlewares怎么使用的?
15. 怎么做的链接复用,怎么支持的并发请求的,go的netpoll是怎么实现的?像阻塞read一样去使用底层的非阻塞read
17. 父goroutine退出,如何使得子goroutine也退出
18. 如何拿到多个goroutine的返回值,如何区别它们
20. 同一个协程里面,对无缓存channel同时发送和接收数据有什么问题
1. new和make的区别
1. new可以看做是C++里面的new,要求传入一个类型:创建一个指针&&申请内存&&初始化为0,在使用时要用*解引用
2. make是go里面新多出来的,slice、map、chan,返回的类型是类型本身
2. Golang的内存管理 ?
3. 调用函数传入结构体时,应该传值还是指针?为什么?
1. 效率对比
1. 在不发生内存逃逸的情况下,传递指针不会发生内存拷贝,效率比传递值更高
2. 发生内存逃逸,传递指针的效率要比传递值更慢
发生内存逃逸的本质是因为该变量的作用于被扩大。
2. 是否能修改传入参数的值
指针可以,值不可以
4. 线程有几种模型?
1对1,多对1,多对多
5. Goroutine的原理了解过么?
原理,其实就是问GMP模型,
1. 先回答GMP分别表示什么
G:每个 Goroutine 对应一个 G 结构体,G 存储 Goroutine 的运行堆栈、状态以及任务函数
M:对OS内核级线程的封装,数量对应真实的CPU数(真正干活的对象)
P:逻辑处理器,即为G和M的调度对象,用来调度G和M之间的关联关系,其数量可通过 GOMAXPROCS()来设置,默认为核心数
2. 再回答GMP调度机制:stealing机制、hand-off机制
6. 讲下Goroutine实现和优势
1. 用户态实现的协议栈,不涉及到内核态和用户态之间的切换,更轻量,上下文切换代价小
2.内存占用少,线程栈2M,协程栈2K
7. Goroutine什么时候会发生阻塞?阻塞的话调度器会怎么做?
7.1. 协程阻塞的场景:协程无法释放的场景
1. 读nil的channal
2. 协程中出现死循环
3. 协程中出现死锁
4. 执行系统调用等待结果返回
5. 数据操作的IO、等待网络请求的返回
7.2. 阻塞的话,调度器会怎么做?参考:7.8.(场景10)G发生系统调用/阻塞
协程G与线程M绑定,为了不阻塞线程M绑定的本地队列里面的P,需要给队列P找一个新的M绑定:若空闲队列中存在空闲的M;若不存在,则判断”普通线程+自选线程”是否小于GOMAXPROC,如果小于创建新的M接管P;如果大于,将P放入空闲P队列中
7.3. 如果Goroutine一直占用资源怎么办,GMP模型怎么处理这个问题?
答:如果有一个Goroutine一直占用资源的话,GMP模型会从正常模式转为饥饿模式,通过信号协作强制处理在前面的Goroutine去分配使用 ==> 引出Goroutine的Mutex锁机制
7.4. Goroutine的锁Mutex机制了解过吗?Mutex有哪几种模式?Mutex锁底层如何实现?
互斥锁的结构体成员有2个,分别是state、sema
1. 加锁和解锁:通过atomic包提供的原子性操作state字段
2. sema是信号量,主要用于等待队列

mutex有2种模式,饥饿模式是1.9版本引出的
1. 正常模式
1. 加锁过程
一个尝试加锁的Goroutine会先自旋几次,尝试通过原子操作获得锁(假设G获取到锁,那么state=1)
若几次自旋之后,仍不能获得锁,则通过信号量排队等待;所有的等待着会按照FIFO的顺序在等待队列中排队。(此时G1、G2、G3...获取不到锁,会在信号量的等待队列中)

2. 释放锁+抢锁过程
当锁锁被G释放(state=0)后,第一个等待者被唤醒后,并不会直接拥有锁,而是需要和 后来者(处于自旋状态&&尚未进入等待队列的Goroutine)进行竞争。
这种情况下,后来者更有优势(获取锁的概率更大),原因是 ①一方面,后来者Goroutine是处于自旋状态,正在CPU上运行,自然比刚唤醒的Goroutine更有优势 ②另一方面,处于自旋状态的Goroutine有很多,而被唤醒的Goroutine只有一个

没有抢到锁的Goroutine,会重新被插入到信号量等待队列的头部。
3. 加锁等待的时间超过1ms

当一个Goroutine为了获取锁等待的时间超过了1ms后,会把当前mutex从正常模式切换到饥饿模式。
2. 饥饿模式
在饥饿模式下,
1. mutex的所有权从执行unlock的Goroutine,直接传递给等待队列头部的Goroutine
2. 后来者不会自旋,也不会获得锁(即使mutex处于unlock状态),它们会直接排队到等待队列的尾部

当一个等待这个获得锁的Goroutine之后,当发生以下2种情况时,将会从饥饿模式切回正常模式:
① 获得锁的Goroutine的等待时间<1ms
② 获得锁的Goroutine是等待队列中的最后一个等待者,此时等待队列为空
分析总结:
1. 正常模式
优点:在正常模式下,自旋和排队是同时存在的,Goroutine在尝试加锁时,会自旋,尝试过几次后如果还没有获取到锁,会进入排队状态。这种在排队之前先让大家来抢的模式,能够有更高的吞吐量(因为频繁的挂起、唤醒Goroutine会带来较多的开销)。
缺点:可能会出现等待队列尾端Goroutine迟迟抢不到锁的情况,即尾端延迟
2. 饥饿模式
饥饿模式下,为了解决尾端延迟问题,不再自旋尝试,所有Goroutine都要排队,严格的FIFO
8. 在GMP模型中Goroutine有几种状态,线程几种状态
线程状态:2种,即去抢占G的时候,会有一个自旋和非自旋的状态
Goroutine状态:

1. idle:空闲状态,刚刚被分配并且还没有被初始化
2. runnable:没有执行代码,没有栈的所有权,存储在运行队列中(等待被调度)
3. running:正在运行。可以执行代码,拥有栈的所有权,被赋予了内核线程M和处理器
4. syscall:正在执行系统调用。拥有栈的所有权,没有执行用户代码,被赋予了内核线程M,但是不在运行队列上
5. waiting:运行时被阻塞。没有执行用户代码并且不在运行队列上,但是可能存在于channel的等待队列上
6. dead:没有被使用,没有执行代码,可能有分配的栈
7. copystack:栈正在被拷贝,没有执行代码,不在运行队列上
8. preempted:由于抢占而被阻塞,没有执行用户代码并且不在运行队列,等待被唤醒
9. scan:GC正在扫描栈空间,没有执行代码,可以与其他状态同时存在
9. 若干线程中,有个线程OOM会怎么样?Goroutine发生OOM呢?怎么排查呢?
1. 线程发生OOM,也就是内存溢出,发生OOM的线程会被kill掉,其他线程不会受到影响
2. go中的内存泄露一般都是Goroutine泄露,就是Goroutine没有被关闭或者没有添加超时控制,让Goroutine一直处于阻塞状态,不能被GC
Go发生内存泄露的场景: 在Go中内存泄露分为暂时性内存泄露和永久性内存泄露
1. 暂时性内存泄露
1. 获取长字符串中一段导致长字符串未释放
2. 获取长slice中一段导致长slice未释放
3. 在长slice新建slice导致泄露
解释:string相比切片少了一个容量的cap字段,可以把string当成一个只读的切片类型。获取长string或者切片中的一段内容,由于新生成的对象和老的string或者切片共用一个内存空间,会导致老的string和切片资源暂时得不到释放,造成短暂的内存泄漏
2. 永久性内存泄露
1. goroutine永久阻塞而导致泄漏
2. time.Ticker未关闭导致泄漏
3.不正确使用Finalizer导致泄漏
怎么排查内存泄露?pprof
10. defer可以捕获到子Goroutine的panic吗?
defer只能捕获本层的panic,不能捕获子Goroutine的panic
11. 开发用过gin框架么?参数校验怎么做的?中间件middlewares怎么使用的?
11.1. 参数校验怎么做的
gin框架使用http://github.com/go-playground/validator进行参数校验 在 struct 结构体添加 binding tag,然后调用 ShouldBing 方法,下面是一个示例
type SignUpParam struct {
Age uint8 `json:"age" binding:"gte=1,lte=130"`
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
RePassword string `json:"re_password" binding:"required,eqfield=Password"`
}
func main() {
r := gin.Default()
r.POST("/signup", func(c *gin.Context) {
var u SignUpParam
if err := c.ShouldBind(&u); err != nil {
c.JSON(http.StatusOK, gin.H{
"msg": err.Error(),
})
return
}
// 保存入库等业务逻辑代码...
c.JSON(http.StatusOK, "success")
})
_ = r.Run(":8999")
}
11.2. 中间件middlewares怎么使用的?
中间件middlewares使用use方法,gin的中间件其实就是一个HandlerFunc,只要我们实现一个HandlerFunc,然后作为参数传递进去
func costTime() gin.HandlerFunc {
return func(c *gin.Context) {
//请求前获取当前时间
nowTime := time.Now()
//请求处理
c.Next()
//处理后获取消耗时间
costTime := time.Since(nowTime)
url := c.Request.URL.String()
fmt.Printf("the request URL %s cost %v\n", url, costTime)
}
}
11.3. gin的route实现原理
1. gin 的每种方法(POST, GET ...)都有自己的一颗树,当然这个是根据你注册路由来的,并不是一上来把每种方式都注册一遍
2. 当 gin 收到客户端的请求时, 第一件事就是去路由树里面去匹配对应的 URL,找到相关的路由, 拿到相关的处理函数(找到对应的 handler)
12. 反射
12.1. 反射的原理
12.2. 反射的使用场景
校验包:"github.com/go-playground/validator"
12.3. 用代码实现反射
13. Channel的特点和实现原理
1. 有缓存和无缓存channel的区别
2. chan的相关问题,如关闭一个已经关闭的chan会如何,有缓存和无缓存的区别是什么?
14. 优雅退出
- 注册一个信号函数,监听信号,在信号触发时,执行优雅退出的函数gracefullyQuit
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
func gracefullyQuit() {
fmt.Println("执行优雅退出的程序")
}
func main() {
fmt.Println("main start")
defer func() {
fmt.Println("byte")
gracefullyQuit()
}()
// 注册一个信号函数,监听信号,在信号触发时,执行优雅退出的函数gracefullyQuit
sig := make(chan os.Signal)
signal.Notify(sig, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGHUP)
go func() {
for s := range sig {
switch s {
case syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGHUP:
gracefullyQuit()
if i, ok := s.(syscall.Signal); ok {
os.Exit(int(i))
} else {
os.Exit(0)
}
}
}
}()
time.Sleep(10 * time.Second)
}
15. 怎么做的链接复用,怎么支持的并发请求的,go的netpoll是怎么实现的?像阻塞read一样去使用底层的非阻塞read
答:go的netpoll底层就是对IO多路复用的封装,底层实现其实和libco的协程框架一样,就是一个调度器、触发机制(超时触发/事件触发)等等
调用read等函数时,实际上会发生协程切换
16. 2个协程交替打印字母和数字?
17. 父goroutine退出,如何使得子goroutine也退出
说明:
1. 父goroutine退出,子goroutine实际上是不会结束的(子goroutine仍然在执行)
2. goroutine虽然不能强制结束另外一个goroutine,但是它可以通过channel通知另外一个goroutine,让其结束
父goroutine关闭子goroutine
方式1. 通过channel通知
package main
import (
"fmt"
"time"
)
func cancelByChannel(quit <-chan time.Time) {
for {
select {
case <-quit:
fmt.Println("cancel goroutine by channel!")
return
default:
fmt.Println("Im alive")
time.Sleep(1 * time.Second)
}
}
}
func main() {
quit := time.After(5 * time.Second)
go cancelByChannel(quit)
time.Sleep(10 * time.Second)
fmt.Println("Im done")
}
// Im alive
// Im alive
// Im alive
// Im alive
// Im alive
// cancel goroutine by channel!
// Im done
方式2. 通过cancelCtx
通过channel通知goroutine退出还有一个更好的方法就是使用context。没错,就是我们在日常开发中接口通用的第一个参数context。它本质还是接收一个channel数据,只是是通过ctx.Done()获取
package main
import (
"context"
"fmt"
"time"
)
func cancelByContext(ctx context.Context) {
for {
select {
case <-ctx.Done(): // Done()是监听cancel()、超时操作
fmt.Println("cancel goroutine by context!")
return
default:
fmt.Println("Im alive")
time.Sleep(1 * time.Second)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go cancelByContext(ctx)
time.Sleep(10 * time.Second)
cancel()
time.Sleep(5 * time.Second)
}
18. 如何拿到多个goroutine的返回值,如何区别它们
1. 将获取的结果写入channel中
2. 遍历channel,获取所有的结果
19. 热重启
1. 功能:保证服务可用性的手段。
2. 过程:它允许服务重启期间,①不中断已经建立的连接,②老服务进程不再接收新连接请求,③新连接请求将在新服务进程中受理,④原服务进程中已经建立的连接,也可以将其设置为读关闭,等待平滑处理完连接上的请求及连接空闲后再退出。
用人话解释,
- 旧版本为退出之前,需要先启动新版本;
- 旧版本继续处理完已经接受的请求,并且不再接受新请求;
- 新版本接受并处理新请求的方式;
3. 优点:通过这种手段,可以保证已经建立连接不中断、连接上的事务可以正常完成、新服务进程也可以正常接收连接、处理连接上的请求
4. 原理:信号+fork
- 父进程监听重启信号
- 在收到重启信号后,父进程调用 fork ,同时传递 socket 描述符给子进程
- 子进程接收并监听父进程传递的 socket 描述符(新的连接都请求到子进程)
- 在子进程启动成功之后,父进程停止接收新连接,同时等待旧连接处理完成或超时
- 父进程退出,热重启完成(子进程接替父进程,继续工作)
20. 同一个协程里面,对无缓存channel同时发送和接收数据有什么问题
1. 同一个协程里,不能对无缓冲channel同时发送和接收数据,如果这么做会直接报错死锁。
2. 对于一个无缓冲的channel而言,只有不同的协程之间一方发送数据&&一方接受数据才不会阻塞。channel无缓冲时,发送阻塞直到数据被接收,接收阻塞直到读到数据。
22. 服务能开多少个m由什么决定?开多少个P有什么界定
1. m和g开多少由内存决定,一个m=2M,一个g=2k
2. m的个数 > g的个数
3. p的个数由GOMAXPROC决定,可以设置
更多推荐

所有评论(0)