1 GOLong对比各语言异同。

C/C++:直接编译成机器码,不需要执行环境,所以一次编码只能适用于一种平台

​ 需要自己处理GC问题

JAVA:编译为中间码(字节码)

​ 需要特定的执行环境(JVM)所以一次编译多处运行,但是有虚拟化损失

JS(原生):不需要编译,直接解释执行,需要执行环境(浏览器),也有虚拟化损失

GOlang

  • 直接编译为二进制,没有虚拟化损失

  • 而且GO自带运行环境,无需处理GC问题

  • 一次编译使用多种平台,(代码基本不用改,在不同系统上编译即可)

  • 很强的高性能并发能力,并且易用性也很好

2 Runtime的特点

运行时:本质就是程序的运行环境(比如JAVA的JVM)

GO的Runtime:作为程序的一部分打包进二进制,随用户程序一起运行。(在编译时会自动导包引用runtime.a)

Runtime能力:内存管理、GC能力、通过协程调度让他有很强的并发能力

Runtime其他特点:有一定的屏蔽系统调用能力,Go的一些关键字就是Runtime的函数(go->newproc,new->newobject,

make->makeslice、makemap等)

3 Go程序是如何编译的?

go build -n 输出编译过程

编译出一堆.a文件(机器码),通过link.exe连接出可执行文件

先编译后连接

分编译前端(词法分析->语法分析->类型检查->中间代码生成)和编译后端(代码优化->机器码生成)

词法分析->语法分析->类型检查->中间代码生成->代码优化->机器码生成——> 连接

词法分析:解析源代码文件,将源代码翻译成token(最小语义结构)

语法分析:token通过语法分析器转换成抽象语法树(AST)

类型检查:遍历抽象语法树,保证节点不存在类型错误,还会展开和改写一些内置函数比如make—>runtime.makemap等。

中间码生成:为了处理不同平台的差异,先生成中间代码(使用了SSA特性)先设置环境变量GOSSAFUNC=main再build看。

机器码生成:输出.a的机器码文件

4 Go程序如何运行起来——理解即可

首先go程序的入口不是main方法,而是rt0_xxx.s汇编文件(rt是runtime,0应该是代表入口)

在rto文件中先是两行代码讲argc和argv(命令参数数量和值)放到寄存器中,随后初始化不归调度其管理的g0协程,g0为了调度协程的母协程。随后有运行时检查包括各种类型长度。检查结构体字段偏移量等基本检查。然后初始化调度器初始化,创建runtime.main协程然后放入调度器等待调度,初始化一个M用来调度主协程,最后主协程执行主函数(runtime.main调用用户main)。

5 GO语言是面向对象的吗?

官方说是-yes or no 。

Go允许面向对象编程风格:

go的继承其实是组合,组合中的匿名字段通过语法糖达到类似继承的效果,底层是一层一层的成员点出来的。

go的封装用首字母大小写实现,大写为公有、小写私有

go使用接口的这种特性来实现多态

6 什么变量的大小是0字节?(空结构体)

空结构体:空结构体占据的大小为 0 但是有地址,多个独立出现的空结构体的地址相同->zerobase(所有长度为0的地址)。

空结构体为了节约内存:

  1. 比如我们想要map只想存key不想存value,value就可以用空结构体(set)

  2. channel只想发一个信号的话可以用空结构体,优化内存占用

int:跟系统字长有关系,64位/8字节,32为/4字节(对应int64 和 int32)

指针和int一样跟系统字长有关系

7 数组、字符串、切片底层?

字符串:其实操作是stringStruct结构体,两个成员str Pointer指向字节数组 和 len int表示byte数组长度

字符串切分,(因为编码问题)先转为rune数组在切片再转为string例如:string([]rune(s)[:3])

切片:切片是对数组的引用,slice结构体有三个字段pointer指向数组、len(切片引用长度)和cap(底层数组长度)。

切片的三种创建原理

  • 字面量创建:s:=[]int{1,2,3}看汇编的话,也是先创建数组,在创建slice结构体在把pointer、len、cap塞进去。
  • 运行时创建:make,看汇编的话,是调用运行时的makeslice()方法。根据传入的参数将指针返回

8 切片容量增长,append追加如何追加:

分两种情况

1)不扩容:编译器只调整len

2)扩容:调用growslice()由于数组必须是连续的内存空间,所以需要开辟新的内存空间。

  • 当切片长度小于1024,翻倍
  • 当切片长度大于1024,加%25

以上的规则是网上的各种说法,不过如果阅读源码growslice()后半部分是有做内存对齐的,对齐之后要大于2或者1.5倍

注意:切片扩容时,是并发不安全的要加锁,因为在扩容时会有不同协程分别对切片进行追加和查看操作,由于扩容会新建数组并修改指针,如果由协程还在读老数组的话就会出现问题。

9 切片作为函数参数会被改变吗?

Go语言中函数参数的传递只有值传递,不过不管是传递的是slice还是slice指针,如果改变底层数组,都会反映到底层数据。因为底层数组在slice结构体中是一个指针。

10 Map

HashMap分为开放寻址法和拉链法。

开放寻址法,底层是一个数组,对k-v进行哈希取模(数组长度),如果要去的目标地址被占用就向后寻找空闲地址

拉链法:对k-v进行哈希取模(数组长度),底层是一个逻辑数组而每一个槽其实是指针每一个指针挂着链表也就是我们的哈希桶,

Golang中的Map用HashMap实现的,并且使用拉链法解决哈希冲突,底层是hmap结构体,内部比较重要的是buckets,B两个字段,bucket存储K-V键值对,bucket的长度是2的B次方,他其实是一个指针指向新的结构体bmap就是我们的哈希桶,桶里面源码规定最多装8个键值对,这些key之所以落入一个桶里是经过哈希取模之后得到的结果是一类(并不相同),通过低B位确定桶号再高8位决定落入那个槽位。我们这个bmap结构体中分别有存储哈希值的tophash和存储键值对的key和values以及指向溢出桶的指针overflow。首先k和v各自放在一起的,可以节省大量的padding填充以节省内存空间。因为每个backet最多存8个键值对,当有第九个键值对时存入overflow指向的溢出桶,而我们溢出桶有这个nextflow字段连接写一个溢出桶也就是链表法。

定位与赋值:通过两层大循环外层是哈希桶和overflow,里层是槽位。赋值操作时map有一个写标志位flag如果是1表示其他协程正在写操作,如果在进行写操作的话会panic所以并不是协程安全的

11 Map扩容

首先golang的map扩容分为两个场景。

第一个场景就是当装载因子超过阈值(源码中定义阈值是6.5),就表示元素太多而我们的bucket又太少,策略就是翻倍扩容也就是B+1。然后我们的hmap中的buckets指向新分配的桶,oldbuckets指向旧桶,也是因为B+1所以意味着bucket是原来的两倍,逻辑上是原来的一个桶变成两个桶,由于我们是取后B位来确认落入那个桶所以改为B+1后每个旧桶中的键值对都会分配到新扩容的哈希桶中。

第二个场景就是装载因子没有超标但是溢出桶较多也会扩容,当B小于15并且溢出桶数量大于2的B次方和当B大于15且溢出桶大于2的15次方,扩容策略是等量扩容,这种情况说明溢出桶过多但是很多bucket并没有装满,所以要将旧的bucket中的元素移动到新的bucket,使bucket中的key排列更紧密提高利用率,

由于可能会有大量的键值对需要迁移,如果一次性迁移会影响性能,所以采用了渐进式的方式,每次只迁移两个bucket。

12 Map中的Key为什么无序?

由于扩容机制,会发生大量key的搬迁,落在不同的bucket中,而遍历的过程是顺序遍历,所以key是无序的。但是虽然是顺序遍历在没有搬迁的情况下也是无序的,因为并不会从0号bucket开始遍历,而是每次都从一个随机号的bucket中的一个随机的槽位开始,即使是写死的map遍历时也几乎是无序的。

map不能顺序读取,是因为他是无序的,想要有序读取,

首先的解决的问题就是,把key变为有序,所以可以把key放入切片,对切片进行排序,遍历切片,通过key取值

13 Map是线程安全的吗?

在赋值、遍历、删除的过程中都会监测写标志位,一旦发现写标志位为1则panic,在赋值和删除函数监测完写标志位是0之后,现将写标志位置为1在进行之后的操作。

14 Map中的元素为什么不能取地址?

如果获取到地址了,一旦发生扩容,k和v的位置都会发生改变,获取的地址也就失效了

15 Map可以边遍历边删除吗?

同一个协程内边遍历边删除,并不会检测到同时读写,理论上是可以边遍历边删除。但是由于Key是无序的所以遍历的结果就可能不相同。

16 sync.Map

sync.Map的底层结构体主要用read和dirty两个字段实现读写分离(扩容与不扩容分离)。两个字段都维护了一个元素为key-entry的一个map,这个entry是一个指针他们指向同一个value。read map本身具备原子性还维护了记录是否有追加操作的标识符,对元素进行查找、更新、删除操作有优先权。而当有追加数据操作的时候使用dirty map。会先上锁在对dirty进行追加操作,此时read map维护的标识符置改为true,这时候两个map就不一致了,如果read map未命中misses字段+1,当misses>=dirty map的元素个数的时候发生dirty提升:read指向dirty map,dirty会置为空然后重建dirty map。适用于读多写少、新建少的场景

对于删除操作分两种情况正常删除和追加后删除:

正常删除:dead map与dirty map是一样的并没有追加,直接走read map将value指针置为空,GC会自动将value回收因为已经没有指针指向它了。

追加后删除:会先从read map找,找不到的话再去dirty map将value指针置为空。不过会涉及dirty提升的问题,会将nil改为expunged提醒在新建dirty map的时候将不会指向这个value。

17 make和new的区别?

都是用来分配内存的函数,

make用于slice、map、channel,返回的是一个引用

new用于int、数组、结构体等值类型。返回的是指向类型的指针

18 接口中iface与eface区别(空接口与接口)

用于描述接口的底层结构体有两个,分别是iface和eface,其中iface描述的接口包含方法,而eface则是不包含方法的空接口。

空接口可以接受任意类型数据,用来描述他的结构体就eface,有_type和data两个字段,__type指向类型元数据,data指向具体的值。

iface结构体中维护两个字段tab和data,data字段是指向接口具体的值,一般是一个指向堆内存的指针,而tab字段指向itab结构体,包括inter字段记录着方法列表等信息,_type指向动态类型元数据,hash字段记录类型哈希值用于快速判断类型是否相等使用,fun字段是一个数组记录这动态类型实现那些接口要求的方法地址。数组大小为1存储的是第一个方法的函数指针用于快速定位方法,无需再类型元数据中查找。

还有一点就是itab结构体是可以复用的,go会将用到的itab结构体缓存起来,通过动态类型+接口类型的哈希值做异或运算作为Key,以itab结构体指针为Value,构建哈希表。

每种类型都有自己的类型描述信息,也就是类型元数据它包括类型名称、大小、对齐边界等。一般放在_type结构体中定义。如果是自定义类型还会有uncommonttype结构体记录着方法数、方法元数据、偏移量等信息。

19 接口的类型断言

空接口断言是通过确定eface结构体中_type字段是否指向断言类型的类型元数据。

非空接口断言,因为itab结构体都会以哈希表的方式缓存起来,通过接口类型+动态类型组合的Key查找对应的itab指针,所以只需要一次比较就能完成。断言失败的类型对应的itab结构体也会缓存起来并且fun数组的第0位置位0,用来标识这里的动态类型并没有实现对应的接口。

20 方法的值接受者与指针接受者

方法和函数的区别就是方法有一个接收者,接受者分为值接收者和指针接收者,我们实现值接收者的方法Go会自动生成对应的指针接收者,但是实现指针接受者的时候并不会自动实现值接受者。

21 Channel通道有哪些应用

  1. 停止信号:一般是关闭某个channel或者向channel发送一个信号。
  2. 定时任务:与定时器结合实现超时控制与定期执行某个任务
  3. 解耦生产者和消费者:工作协程不断从工作队列取任务,生产者只管往channel发送任务,解耦了双方,
  4. 控制并发数:用缓冲型channel,存看做许可证的数据,当要执行协程时先取出许可证,当执行完毕后放回许可证,就能限制协程数量。

22 Channel通道

不要通过共享内存来通信,而是通过通信共享内存。可以避免协程竞争和数据冲突的问题,模块之间更容易解耦。

底层数据结构是hchan结构体,由五个字段构成环形缓存区,有一个指向第一个缓存数据的指针,以及数据的类型和大小的字段elemsize和elemtype,最后是缓存队列的大小和长度qcount和datasize。hchan还有等待发送和接收的两个以双向链表实现的goroutine队列,以及表示当前可以发送或者接收的goroutine索引值的两个字段,channel用closed字段做一个是否被关闭的标志,最后还有一个保护每个读写channel的操作都是原子操作的互斥锁来构成channel。

环形缓冲区的好处:

可以减少GC开销,如果是正常的队列前面新增数据要不断开辟内存,后面删除数据又要回收数据,环形缓存的话可以固定队列大小减少GC开销。

23 Channel发送与接收数据原理

Channel发送数据原理

向Channel发送数据C<-会被编译器转化成runtime.chansend()函数,这个函数的大致业务逻辑是:

  • 如果channel没有缓存或者缓冲区为空并且有读等待的goroutine,会按照先进先出的规则直接唤醒goroutine将数据拷贝给goroutine。
  • 如果缓冲区没有装满,先获取可存入的缓冲区地址存入数据,也需要维护索引qcount和send都进行++操作。
  • 如果没有接收队列为空或者缓冲区已经满了,自己进入发送队列休眠。

Channel接收数据原理

接收数据的话编译器会将<-c会根据接收参数是否带“ok”是否成功接收的bool转化为chanrecv1()或者chanrecv2(),不过最后都是调用chanrecv()函数。逻辑是:

  • 如果没有缓存并且发送等待队列里有Goroutine,直接将数据拷贝过来并且唤醒goroutine。
  • 如果有缓存,会直接从recvx的索引位置取出数据,然后把数据从缓存区清除,就可以将发送等待队列中的goroutine的数据放进缓冲区并且唤醒它。
  • 如果没有缓存并且发送等待队列也没有goroutine,自己就进入接收等待队列。

Channel收发数据本质是值的复制,有缓冲的channel先把发送方G的值拷贝到自己维护的数组,再拷贝到接收G,而非缓冲型的则直接从发送栈数据拷贝到接收栈空间。

24 非阻塞的Channel怎么做

使用select可以进行非阻塞的接收。

select{case <-ch1:fmt.Prientln("ch1")case ch2<-1:fmt.Prientln("ch2")default:fmt.Prientln("none")
}

25 从一个关闭的通道可以读数据吗?

当channel有缓冲区,当被关闭的时候可以继续读出有效值,只有当返回的是否成功接收的bool为false时读出的数据是无效的。

26 Go goroutine协程与线程区别

可以从内存消耗、他们的创建和销毁、切换三个方面说:

1.内存消耗

创建一个线程消耗1MB栈内存,而且还需要guard page与其他线程的栈空间进行隔离。而创建一个goroutine栈内存消耗为2k,实际运行过程中,如果空间不足会自动扩容。

2.创建与销毁

线程的创建和销毁因为要和操作系统打交道是内核级的所以会有不小的消耗,通常会用线程池解决尽量去复用减少开销。而goroutine由go的运行时管理,创建和销毁消耗非常小是用户级的。

3.切换

线程切换时需要保存很多寄存器,方便恢复。而goroutine只需保存三个寄存器(program counter、stack、BP)。

27 goroutine三种状态

  • Waiting:等待状态,goroutine在等待某事件的发生,比如等待网络数据、磁盘IO等
  • Runnable:就绪状态,只要分配给M就能运行
  • Runing:运行状态,在M上运行。

28 Go sheduler 调度(GMP模型概括)

Go的Runtime维护所有的goroutine,并通过sheduler进行调度。

调度通过GMP这三个基础结构体来实现。

G是表示一个goroutine,M表示内核线程,P代表一个虚拟的处理器,他维护一个处于runnable状态的goroutine队列。M只有获取P才能执行G。

  • 首先每个P都保存runnable状态的 goroutine的局部队列,当 P 的的局部队列已经满了之后就会把 goroutine 放到全局队列。
  • 每个 P 和一个 M 绑定,M 是真正的执行 goroutine 的实体。M 从绑定的 P 中的局部队列获取goroutine 来执行。
  • 当 M 绑定的 P 的局部队列为空时,M 会根据调度顺序来获取G。顺序是先看自己的本地队列,再去看全局队列,如果没有再去网络轮询器找,最后才会从其他P中偷取。
  • 当 goroutine 因系统调用(syscall)阻塞时,它会阻塞 M,此时 P 会和 M 解绑即 hand off,并寻找新的M,若没有 idle 的 M 就会新建一个 M。
  • 当 goroutine 因 channel 或者 网络I/O 阻塞时,不会阻塞 M,M 会寻找其他 runnable 的 G;当阻塞的goroutine 恢复后会重新进入队列等待执行。

GMP底层结构分别对应着runtime.g.m.p结构体:

g结构体,里面比较重要的字段有:

  • stack字段指向stack结构体,stack中有分别指向协程栈高低地址的两个字段hi和lo。
  • sched字段底层是gobuf结构体,表示目前运行的现场,有sp栈指针和pc程序计数器等信息。
  • atomicstatus字段指的是goroutine的状态
  • goid字段指的是goroutine的ID。

m结构体:用来记录线程的一些信息,当前线程绑定的P(有一个P字段)以及在M上运行的G的信息,有两个g分别是g0字段和curg字段,g0字段是我们的g0协程,用来操作调度器,curg字段(current g)是目前M正在运行的goroutine。M有自旋和非自旋两种状态,自旋的时候努力找工作,找不到工作就会进入非自旋状态,之后就会休眠,直到有工作处理的时候其他线程通过park字段唤醒该M,被唤醒进入自选状态。

p结构体:为M的执行提供上下文,保存M执行G的一些资源,例如本地可运行队列runq(最多可以存储256个待执行任务)。通过runqhead和runqtail指向局部队列头和尾。

GMP相互配合共同实现 go scheduler机制。G需要在M上才能运行,M依赖P提供的资源,P则持有运行的G。

29 抢占式调度——(触发调度)

在1.2之前不支持抢占式调度,只能依靠goroutine主动让出CPU资源才能触发调度,这会引起一些问题,比如某些goroutine长时间占用线程造成其他goroutine饥饿,以及垃圾回收需要先等goroutine先停下来在进行,也会造成长时间等待的现象。

1.2版本实现了基于协作的抢占式调度,流程是:

  • 编译器会在调用函数前插入runtime.morestack,让运行时有机会在这段代码中检查是否需要执行抢占调度
  • Go运行时会在垃圾回收、以及系统发现goroutine运行超过10ms就会给协程设置一个抢占标记
  • 当发生函数调用时,可能会调用runtime.newstack,检查抢占标记,如果有抢占标记就会触发抢占让出CPU。

这种方法只能解决部分问题,因为只有在有函数调用的地方才能插入抢占代码,对于没有函数调用而是纯算法循环的计算的goroutine,依然无法抢占。

1.4版本实现基于信号的抢占式调度(异步抢占

不管协程有没有意愿让出CPU使用权,只要执行时间过长,就会发送信号强行夺取CPU使用权。具体流程:

  • M注册一个sigurg信号的处理函数:sighander
  • sysmon启动后会间隔性的监控,最长间隔为10ms,最短20us,如果发现某协程独占P超过10ms会给M发送抢占信号
  • M收到信号后,内核执行sighander函数把当前协程状态从running改为runnable状态。放到全局队列里面,M继续寻找其他goroutine运行。
  • 最后被抢占出去的G还会再次被调度执行。

30 Golang的g0和m0是什么?

m0 是 Go Runtime 所创建的第一个系统线程,一个 Go 进程只有一个 m0,也叫主线程。

从多个方面来看:

数据结构:m0 和其他创建的 m 没有任何区别。

创建过程:m0 是进程在启动时应该汇编直接复制给 m0 的,其他后续的 m 则都是 Go Runtime 内自行创建的。

goroutine 一般分为三种,分别是:

  • 执行用户任务的叫做 g。
  • 执行 runtime.main 的 main goroutine。
  • 执行调度任务的叫 g0。

g0 比较特殊,每一个 m 都绑定一个 g0。在 g0 的赋值上也是通过汇编赋值的,其余后续所创建的都是常规的 g。

从多个方面来看:

数据结构:g0 和其他创建的 g 在数据结构上是一样的,但是存在栈的差别。在 g0 上的栈分配的是系统栈,在 Linux 上栈大小默认固定 8MB,不能扩缩容。而常规的 g 起始只有 2KB,可扩容。

运行状态:g0 和常规的 g 不一样,没有那么多种运行状态,也不会被调度程序抢占,调度本身就是在 g0 上运行的。

31 新建协程调度顺序

  • 随机寻找一个P
  • 将新协程放入P的runnext进行插队
  • 如果P的本地队列满了再放入全局

go的调度会优先执行新建协程在

newproc函数里调用了runqput方法里面讲了这个规则具体逻辑是:

当参数next为true时将新协程放入P的runnext进行插队,要是为false也分两种情况当本地队列满了放全局,没满放本地。

32 M:N模型是什么(多对多)

Go的运行时在启动后会按需创建N个线程之后创建M个协程也就是goroutine都会依附在N个线程上执行。

33 工作窃取是什么?

Go调度器的职责就是将所有处于runnable的goroutine均匀调度在P上运行的M,当一个P发现自己的本地运行队列没有goroutine时,会从其他P偷一些goroutine来运行。实际上Go调度器每一轮调度要做的工作就是找到runnable的goroutine并执行它,找的顺序是先看自己的本地队列,再去看全局队列,如果没有再去网络轮询器找,最后才会从其他P中偷取。

34 处理协程太多的方案

1优化业务逻辑以及调整系统资源

2利用channel缓冲区:在启动协程之前在channel送一个空结构体,协程结束之后取出空结构体,让协程数维持在缓冲区以里。

建议适用于新建一批一样的协程,但是系统业务导出都在建协程就很鸡肋。

空结构体可以作为信号使用,不占内存

3协程池tunny:预创建一定数量的协程,将任务送入协程池队列,协程池不断取出可用协程执行任务。

但是Go本身的调度模型就类似一个协程池,二级池化的话增加复杂度,违背Go语言对协程即用即毁,不要池化的初衷。还是要尽量优化业务。

35 context上下文

并发编程的时候,一般用一个goroutine处理一个任务,它可能会有多个goroutine处理子任务,在这个场景李可以通过context传递取消信号,超时时间,截止时间、键值对信息等。

注意点就是:传递键值对的时候key需要用自定义类型包装一下,如果不包装会有值遮挡的问题。

36 反射

反射可以在运行期间,操作任意类型的对象。通过TypeOf获取对象类型,ValueOf获取对象值。

37 defer延迟语句

逆序执行,defer语句会进入栈,按照先进后出的顺序执行,因为后面的函数可能会依赖前面的资源。

defer后面的函数参数是复制一份。

38 recover如何在panic中拯救程序

panic会终止当前协程的运行,panic之后协程还会执行defer,不过主协程直接就崩了,可以用defer 包一个recover,就不会影响其他协程。

39 Go中锁底层机制

先说一下原子操作atomic包,他的底层是在硬件层面加锁的机制,保证操作变量的时候其他线程无法访问,不过只适用于简单变量的简单操作。比如atomic包里有cas从硬件层面实现例如,变量等于a的情况下将b赋值给变量等。

sema锁(信号锁)在底层结构体是一个无符号整形uInt32类型的变量,内部给每一个uInt32都对应一个SemaRoot结构体有一个treap指针指向sudog结构体,sudog里面的指针指向一课由goroutine构成的平衡二叉树树。

如果uInt32>0表示可以并发的获取锁的协程个数;当协程获取锁,先通过atomic.load()再通过atomic.cas()使得uint32-1,释放锁时uint32+1

如果uInt32==0,sema锁就是一个协程等待队列,底层是现将协程放入堆树(当队列用)再用gopark对协程进行休眠等待,等到另一个协程释放锁的时候通过原子操作将nwait-1再从堆树取出再唤醒协程。

40 sync.Mutex互斥锁

底层结构体有两个成员state状态和sema刚才说的信号锁因为没有配置初始值默认值为0,所以被配置成线程等待队列。

state状态是4个字节,最后一位代表锁的状态locked如果是1表示被锁,0表示没被锁。低2位表示Woken表示从睡眠中唤醒的意思,低3位Starving表示饥饿模式的意思,剩下的29位用来做waiterShift记录等待锁的协程数量。

互斥锁有两种模式饥饿模式和正常模式,

先说一下正常模式就是Starving=0的情况,当协程想要获取锁回去判断低1位是否为0,如果为0通过cas将第一位改成1成功获取锁,如果判断低1位为1协程会进入自旋状态等待锁的释放,如果自旋多次都失败就会去获取sema锁,因为sema=0所以此时协程湖休眠进入等待队列,WaiterShift+1。协程解锁也是通过cas操作将第一位置位0,同时看在sema中有没有休眠队列,如果有则需要按照先进先出的顺序唤醒一个协程。不过当此时有新协程需要获取锁的时候会产生竞争问题,有可能导致一个协程一直获取不到锁。

一旦goroutine超过1ms没有获取锁,会直接进入饥饿模式,防止部分goroutine被饿死。饥饿模式中goroutine不会自旋,而是直接休眠进入等待队列尾部等待,互斥锁会直接交给等待队列的首部goroutine。如果等待队列清空或者goroutine的等待时间小于1ms则回到正常模式。

注意点:

1、只给关键的可能会出并发安全问题的地方加锁,不要将整个业务都加上锁,可以避免锁竞争。

2、要确保锁的释放可以用defer

41 sync.RWMutex读写锁

如果需求是只读时只需要不让其他协程修改就行,读是可以多协程共享不需要互斥。

一般读写锁的原理都是分为两个队列一个写互斥锁队列一个读共享锁队列

读写锁的底层结构体是有五个字段,w字段复用互斥锁的能力,以及读写等待队列两个字段,正在读操作的协程数量ReaderCount字段,最后是ReaderWait字段他是表示当前写操作被阻塞的需要读操作的协程数量。

加写锁的逻辑是,如果没有读协程,先去竞争互斥锁,给readerCount赋一个很大的负数变为负值 ,后来的读协程就会进入读等待队列,另一个场景时当有读协程的话,首先也是要竞争互斥锁,然后将readerCount减去一个很大的负数,让后续的读锁知道这里有一个写锁在排队,写锁需要等readerWait=0的时候也就是读锁全释放完之后,就可以加写锁了。

加读锁的逻辑是,如果ReadCount>0,表示目前没有写锁,直接将ReadCount+1加上读锁,如果ReadCount<0表示目前有写锁排队或者在进行写操作,不过依然要ReaderCount+1后看大于还是小于0,依然小于0然后进入读等待队列等待,大于0直接加锁。释放读锁的逻辑是给readerCount-1,如果>0释放成功,如果<0则表示有写锁在队列等待,会判断自己是不是readerWait最后一个读协程,如果是的话唤醒写协程。

42 sync.WaitGroup等待组

WaitGroup有一组协程等待另一组协程完成后在执行的作用。WaitGroup结构体有两个成员noCopy和state1。而state1比较重要,他是以大小为3的uint32类型数组,分别存着正在等待协程的个数waiter,正在运行协程个数counter,以及一个sema等待队列,记录正在等待的协程。

WaitGroup有三个方法Wait()、Done()、Add()。

Wait()方法:要先看一下counter看看前面还有多少个协程还没执行完,如果counter=0直接返回,如果!=0表示还有没执行完的协程,如果有新来的协程通过cas操作将waiter+1,陷入sema等待队列。

Done()方法:实现比较简单通过Add(-1)实现,

Add()方法:用来增加被等待执行的协程数量,通过对counter+1实现。因为Done()是Add(-1)实现的,那么counter减到0的时候说明协程都执行完了,要是等待队列里面有协程,就通过信号量计数通知唤醒sema中的所有协程。

43 sync.Once

可以保证某段代码可以只执行一次,底层结构体只有一个用于标识是否执行过的done字段以及一个互斥锁。

源码里面具体实现是Do()方法会先判断标志位知否为0,如果为0走DoSlow()方法,为当前goroutine获取互斥锁并将标志位改为1,执行传入的方法,最后解锁。

44 sync.Pool

对于频繁地分配、回收内存会给 GC 与CPU带来一定的负担。而 sync.Pool 可以将暂时将不用的对象缓存起来,待下次需要的时候直接使用,不用再次经过内存分配,复用对象的内存,减轻 GC 的压力,提升系统的性能。

45 排查锁异常问题

1、拷贝锁可能会死锁,尤其是拷贝已经加锁的。通过go vet 命令检查锁拷贝

2、RACE竞争监测,可以发现隐含的数据竞争问题

3、go-deadlock死锁监测工具,有一个go-deadlock项目github的一个项目,可以监测死锁,长时间加不上就会报错提醒,与互斥锁用法一样。

46 Go堆和栈的概念?

Go堆栈的概念:

  • 传统意义的栈被运行时消耗完了,用于维护各个组件协调。
  • 对于用户态而言,堆和栈都是申请的堆内存只不过是在逻辑上分为堆和栈
  • 为了防止内存碎片化,会在适当的时候对栈做深拷贝,所以指针运算符不能用。因为无法确认运算前后指针指向的地址内存是否已经移动。

Go栈的作用:协程执行路径、局部变量、函数传参、函数返回值

协程栈的位置位于堆内存上,堆内存位于操作系统虚拟内存上。

47 逃逸分析

当一个对象的引用被多个方法或者线程调用时,那这个指针发生了逃逸,逃逸分析决定了一个变量是分配到堆上还是栈上。编译器会根据变量是否被外部引用来决定是否逃逸。

如果变量在函数外部没有引用,优先放在栈上;不过如果我们定义一个变量申请内存过大超过栈的存储能力会分配到堆上。

如果变量在函数外部存在引用,优先放在堆上;

通过反汇编命令go tool compile -S main.go可以看出是否逃逸;

48 Go内存对齐

CPU访问内存是按照一个字长为单位进行访问的。为了让CPU更快的存取各个字段,GO编译器会把结构体做数据的对齐。编译器通过在结构体的各个字段之间填充一些空白已达到对齐目的,用空间换时间。

Go提供对齐系数,地址需要被对齐系数整除,不同变量有不同的对齐系数。

结构体的内存对齐分为内部对齐与结构体之间对齐:

  • 内部对齐的话每个成员的偏移量是自身大小与其对齐系数较小值的倍数,
  • 结构体之间对齐的话是做结构体长度填充,结构体长度需要是最大成员长度与系统字长中较小的整数倍,不够的填充空白。

所以可以通过调整成员顺序以节省空间。

49 内存分配策略

内存分配策略一般都是线性分配或者链表分配这两种策略

线性分配策略:会从一段连续空间的一端开始按需要顺序的将内存分配,但是它不能再内存被释放时复用内存,会使内存碎片化。

链表分配策略:通过类似链表的数据结构维护未分配的内存,因为分配内存时需要遍历链表,所以时间复杂度为O(n),他就可以复用已经释放的内存。

GO内存分配叫分级分配策略:

  • 首先说一下Go的堆内存mheap是由很多heapArena内存单元组成的,每个单元会管理64MB的内存空间,并且按照预定的规格把内存页划分成块,再把不同规格内存块放入对应的链表中也就是内存管理单元mspan。
  • mspan底层结构体中通过前后两个指针构成双向链表,并且还记录了跨度的起始地址和指向跨度所包含的页数量的一些信息。Go按照对象大小将mspan分为从67种级别1级到67级,还有大于32kb的特殊对象为0级, ,这样可以更好的管理地址空间和可以使对象在内存里排布的更紧密,减少内存浪费和内存碎片化的问题。
  • 不过因为每个heapArena中mspan的等级都不确定,要是想要找到适合自己的空间,如果是通过遍历的方式那性能肯定非常差,所以Go用中心缓存mcentral解决这个问题,类似索引的原理一个mcentral对应一种mspan类型记在spanclass中,spanclass通过高七位标记mspan的大小等级一共68种,低1位标记是否需要GC扫描,所以一共可以分136种。每个mcentral 有两种列表,有空闲和没空闲的列表 。当有内存申请时,就去最合适的mcentral 的有空闲列表去查询,从而找到堆内存中对应的内存块,但是当同类型规格的 mspan 有并发请求的时候,会有竞争问题就需要互斥锁。
  • 不过Go使用类似GMP模型中本地队列的概念通过加入局部缓存mcache,每一个p都拥有一个mcache方便快速无锁的进行内存分配。

如果在64位操作系统上,一个heapArena是64MB,GO进程最多可以申请2的20次方个heapArena,这样设计正好占满进程虚拟内存256TB

50 Go内存管理的对象分级

Go语言将对象按照大小分为三种级别:小于16b的微对象(无指针)、[16b,32k]的小对象、大于32k的大对象。

  • 微对象分配从mcache拿到2级mspan,将对各微对象合并成一个16字节(byte)的块进行管理和释放。
  • 小对象分配会通过查表的方法先计算所需要mspan的级别,如果mcache有空闲内存空间就直接存可以避免分配流程,如果没有可分配的mspan的话需要向中心缓存申请新的mspan进行替换操作,如果中心缓存也慢了的话,需要开辟新的heapArena。
  • 大对象分配是由于超过了最大等级所以直接绕过mcache与mcontral直接通过mheap进行分配。

51 垃圾回收

Go是使用三色标记清除算法+混合写屏障机制的主体并发增量式回收。

三色标记是规定了三种不同类型的对象为白色对象、灰色对象、黑色对象。垃圾回收开始时所有对象都是白色,然后把直接追踪到的根对象(栈上的值、寄存器的值和全局变量)都标记为灰色,灰色对象表示基于当前节点展开的追踪还没有完成,当基于某个节点的追踪任务完成后,把该节点标记为黑色表示为存活数据,就不用基于它再追踪了,而基于黑色节点找到的所有节点标记为灰色,表示还要基于他们继续追踪,如果没有灰色节点时就表示标记工作可以结束了,有用数据为黑色,垃圾为白色,就可以回收这些白色对象的内存。

不过如果是并发或者增量执行,它很难保证标记与清扫过程的正确性,比如悬挂指针,可以通过遵循三色不变式解决:

  • 强三色不变式:黑色对象不会引用白色对象,
  • 弱三色不变式:黑色对象可以引用白色对象,不过必须存在可以到达白色对象的链路中存在灰色对象。

Go通过混合写屏障机制实现强弱三色不变式是将插入写屏障与删除写屏障结合而来,类似一个同步机制,赋值器在进行指针写操作的时候通知回收器。

  • 删除写屏障:如果指向白色对象的指针被删除,白色对象会标记为灰色
  • 插入写屏障:为了防止黑色对象指向白色对象,就当白色对象被引用的时候将它标记为灰色。

GC流程:

标记准备:打开写屏障,需要STW

标记开始:使用三色标记法,与用户程序并发执行

标记终止:对触发写屏障的对象进行重新扫描标记,关闭写屏障,需要STW

清理阶段:将需要回收的内存归还到堆中,将过多的内存归还给操作系统,与用户程序并发执行

STW:stop the world万物静止需要程序暂停,当内存占用到达一定阈值是,程序会被通知暂停,垃圾回收器进行工作

批量写屏障:将需要着色的指针统一写入一个缓存,当缓存满的时候统一着色。

直接分配到栈上的数据会随着函数调用栈的销毁释放自身调用的内存。

52 GC触发时机

分为主动触发和被动触发:

主动触发:通过调用runtime.GC来触发,此时阻塞的等待GC完成

被动触发:也是有两种方式:

  • 使用监控线程,当超过指定时间(两分钟)没有产生任何GC时,强制触发
  • 使用步调算法,核心思想是控制内存增长的比例。每次GC都会在标记结束后设置下一次的触发GC的堆内存分配量。

53 GC优化

会出现的问题:

  1. GC长时间停顿或者执行的过程没有执行用户代码,导致需要立即执行的用户代码滞后问题
  2. 对于频繁分配内存的场景,会增加GC的工作量,占用CPU资源

解决方法:

  1. 调整内存分配的速度,提高CPU的利用率。

  2. 尽量复用已经分配好的内存

  3. 调整GOGC参数,遇到海量请求的时候,可以将GCGO的值改大,减少触发频率。

54 TCP网络编程

操作系统提供socket为网络连接的抽象。

IO模型:同时操作socket的方案:阻塞、非阻塞、多路复用

Linux eopll~多路复用:将注册多个Socker事件,当有时间发生就返回,提供事件列表,不需要查询每个socket。

55 Go是如何抽象Epoll

多路复用抽象层:用于屏蔽各个平台对多路复用的实现,查询事件抽象为netpoll()但返回的不是事件而是返回等待时间的协程列表。通过pollDesc结构体描述协程和相关socket关系,pollcache是一个pollDesc链表头。

Network Poller:抽象类多路复用的操作,也可以自动监测多个Socket状态

56 Golang-gin框架路由原理

1.什么是http路由

撸过http框架的同学都知道,一个MVC模型的http框架肯定是少不了路由这一块的,那么什么是路由呢。

简而言之,http路由即是一条http请求的“向导”,根据URI上的路径,指引该条请求到对应的方法里去执行然后返回,中间可能会执行一些中间件。

####2. 路由的种类

  • 静态路由

框架/用户提前生成一个路由表,一般是map结构,key为URI上的path,value为代码执行点。

优点:只需要读取map,没有任何开销,速度奇快。

缺点:无法正则匹配路由,只能一一对应,模糊匹配的场景无法使用。

  • 动态路由

用户定义好路由匹配规则,框架匹配路由时,根据规则动态去规划路由。

优点:适应性强,解决了静态路由的缺点。

缺点:相比静态路由有开销,具体视算法和路由匹配规则而定。

3.gin框架路由实现原理

gin框架作为一个轻量级的快速框架,采用的是前缀树的方式实现的动态路由

gin框架使用路由

我们以下面代码为例,去看看gin是如何实现路由的。

r := gin.New()
r.GET("/user/:name", routeUser)func routeUser(c *gin.Context){//todo something
}

上面定义了一个路由/user/:name,它会精确匹配/user/:name,而不会匹配/user/user/或者/user/allen/abc

接下来我们跟着源码去探究gin是如何实现的。

初始化框架

r := gin.New()生成了一个Engine对象,Engine对象是整个框架的核心,也包含了对路由的操作和许多成员变量,其中包括路由要执行的任务链Handlers HandlersChain,方法树trees methodTrees等。

定义路由

r.GET("/user/:name", routeUser)定义一个GET请求,模糊匹配/user/:name

步骤1:将相对路径/user/:namejoin为绝对路径,因为有可能是个路由组,前面还有一些路径。路由组稍后再介绍。

步骤2:判断handlersChain的长度,不能超过math.MaxInt8 / 2并且把路由方法装载到handlersChain里面去。

步骤3:装入路由存入engine.trees变量。

步骤4:返回当前对象,达到能使用链式操作的目的。

访问路由

输入localhost:8080/user/abc,首先进入net的ServeHTTP()方法。然后被gin框架handleHTTPRequest()方法接受。循环之前注册的路由树engine.tress。匹配method部分源码gin.go=>handleHTTPRequest()

t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {if t[i].method != httpMethod {continue    //判断请求method是否相等如果不等则continue}root := t[i].root// Find route in treevalue := root.getValue(rPath, c.params, unescape)//匹配路由的核心算法,如果匹配出来的value为空则直接退出。......
}

匹配路由核心算法源码tree.go=>getValue()

func (n *node) getValue(path string, params *Params, unescape bool) (value nodeValue) {walk: // Outer loop for walking the treefor {prefix := n.pathif len(path) > len(prefix) {if path[:len(prefix)] == prefix {//前缀匹配...switch n.nType {case param:...(*value.params)[i] = Param{Key:   n.path[1:],Value: val,}// 匹配出参数}}if path == prefix {// 完全匹配,无需匹配参数...}}}
前缀树算法

前缀树的本质就是一棵查找树,相比普通查找树,它适用于一些特殊场合,比如用于字符串的查找。比如一个在路由的场景中,有1W个路由字符串,每个字符串长度不等,我们可以使用数组来储存,查找的时间复杂度是O(n),可以用map来储存,查找的复杂度是O(1),但是都没法解决动态匹配的问题,如果用前缀树时间复杂度是O(logn),也可以解决动态匹配参数的问题。

下图展示了前缀树的原理,有以下6个字符串,如果要查找cat字符串,步骤如下:

  1. 先拿字符croot的第一个节点a比较,如果不等,再继续和父节点root的第二个节点比较,直到找到c
  2. 再拿字符a和父节点c的第一个节点a比较,结果相等,则继续往下。
  3. 再拿字符t和父节点a的第一个节点t比较,结果相等,则完成。

同理,在路由中,前缀树可以规划成如下:

具体查找方法和上面一致。

57 泛型

func ToString[ T fmt.Stringer](v T)string{

}

用中括号实现,有约束条件通过接口实现,any所有类型

内置类型:扩展了接口,之前只有方法集扩展后的接口支持类型集

~int,就支持int和所有基于int创建的自定义类型

比如:

type Integer interface(

~int | ~int8

func Sum[T Integer ] (s T)(r T){

return

}

Golang知识总结相关推荐

  1. 超详细的golang学习指南,golang知识图谱

    golang知识图谱 基础知识 go 语言关键字.标识符.数据类型.变量.流程控制.函数.数组.闭包 关键字 break - 使用break关键字可以终止循环并继续执行其余代码 case - 这是sw ...

  2. golang知识图谱NLP实战第一节——整体思路

    golang知识图谱NLP实战第一节--整体思路 golang知识图谱NLP实战第二节--解析依存句法分析结果 golang知识图谱NLP实战第三节--关系抽取 最大的愿望是给engineercms工 ...

  3. golang知识图谱NLP实战第四节——关系抽取完善逻辑

    陈同学将hanlp做成了服务:https://gitee.com/Erichan/EngineerCMS-HanLPService 用golang应用提交文字给这个hanlp服务,返回json数据格式 ...

  4. php+使用go编译,golang如何编译

    Go语言中使用 go build 命令主要用于编译代码.在包的编译过程中,若有必要,会同时编译与之相关联的包. go build 有很多种编译方法,如无参数编译.文件列表编译.指定包编译等,使用这些方 ...

  5. golang mysql封装_golang如何封装路由

    封装方式一.路由写在 main函数中,数据库初始连接放在 init() 函数中.. 首先看 main.go 一个初始化函数,初始化 dbfunc init() { db.Connect() } 第二, ...

  6. golang后端php前端,Golang如何接收前端的参数

    使用Golang开发web后台,需要接收前端传来的参数并作出响应,那么Golang该如何接收前端的参数呢?一起来看下吧. Golang如何接收前端的参数 1.首先,创建一个Golang web服务.p ...

  7. php 创建 map,golang如何创建map

    map是一堆键值对的未排序集合,类似Python中字典的概念,它的格式为map[keyType]valueType,是一个key-value的hash结构.map的读取和设置也类似slice一样,通过 ...

  8. VSCode配置Golang单元测试实例

    目录 前言 正文 一.导入testing工具包 二.单元测试文件命名规范 三.单元测试方法命名规范 四.执行单元测试 结尾 前言 说到代码的健壮性,单元测试是少不了的,基本上所有语言都有自己的单元测试 ...

  9. go 通道 返回_GCTT 出品 | Go 语言的缓冲通道:提示和技巧

    通道和 goroutine 是 Go 语言基于 CSP( communicating sequential processes ,通信顺序进程)并发机制的核心部分.阅读本文可以学到一些关于channe ...

最新文章

  1. linux c 结构体初始化的四种方法
  2. 004_SpringMVC分层配置文件
  3. Oracle使用遇到的问题
  4. JavaScript--fullPage.js插件
  5. java集合基础_java常用集合基础知识
  6. django序列化器嵌套_Django Rest Framework中用于OneToOne字段的序列化程序中的嵌套关​​系
  7. 华为考虑对外出售5G芯片 但对象只包括苹果公司
  8. android 机子 启动不进入 android
  9. oracle掉电后ora 00600,ORA-00600: 内部错误代码, 参数: [kcratr1_lastbwr](转)
  10. mysql 存储过程:提供查询语句并返回查询执行影响的行数
  11. 独立样本t检验、方差齐性检验
  12. 基于单片机倾角检测仪设计分享
  13. 如何撰写总体设计与详细设计文档
  14. 高考是不是计算机投档,1:1高考投档是什么意思 填报志愿注意事项
  15. 项目xx方案文档格式规范模板
  16. 【ReID】Pyramidal Person Re-IDentification via Multi-Loss Dynamic Training
  17. Arduino与Proteus仿真实例-HC-SRF04超声波测距仿真
  18. Python 取模运算(取余)%误区及详解
  19. java代码安全检测机制,下列选项中,属于Java语言的代码安全检测机制的一项是______。A.动态链接B.访问权限的检查C.通过接...
  20. ristretto255 point压缩和解压缩算法(2)——extended坐标系下

热门文章

  1. 2021年陕西省大学生程序设计竞赛(重现赛)
  2. [BZOJ4899]:记忆的轮廓(概率DP)
  3. Probits在A $ $当客户端配置文件摔跤手斯科特%verfied_url%斯坦纳
  4. 五子棋小游戏 java版(代码+详细注释)
  5. S50(Mifare 1K)卡简介及存储控制原理
  6. java游戏解救人质_抖音解救人质的游戏
  7. ffmpeg从某站提取视频、音频、详解
  8. 房地产行业erp系统
  9. ubuntu linux下制作win10启动盘
  10. 基础语法篇_10——设置对话框、颜色对话框、字体对话框、示例对话框、改变对话框和控件的背景及文本颜色、位图显示