目录

1. new和make的区别

2. Golang的内存管理 ?

3. 调用函数传入结构体时,应该传值还是指针?为什么?

4. 线程有几种模型?

5. Goroutine的原理了解过么?

6. 讲下Goroutine实现和优势

7. Goroutine什么时候会发生阻塞?阻塞的话调度器会怎么做?

8. 在GMP模型中Goroutine有几种状态,线程几种状态

9. 若干线程中,有个线程OOM会怎么样?Goroutine发生OOM呢?怎么排查呢?

10. defer可以捕获到子Goroutine的panic吗?

11. 开发用过gin框架么?参数校验怎么做的?中间件middlewares怎么使用的?

12. 反射

13. Channel的特点和实现原理

14. 优雅退出

15. 怎么做的链接复用,怎么支持的并发请求的,go的netpoll是怎么实现的?像阻塞read一样去使用底层的非阻塞read

16. 2个协程交替打印字母和数字?

17. 父goroutine退出,如何使得子goroutine也退出

18. 如何拿到多个goroutine的返回值,如何区别它们

19. 热重启

20. 同一个协程里面,对无缓存channel同时发送和接收数据有什么问题

22. 服务能开多少个m由什么决定?开多少个P有什么界定


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决定,可以设置

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐