Golang M 2023 5 * theory

Posted 知其黑、受其白

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang M 2023 5 * theory相关的知识,希望对你有一定的参考价值。

阅读目录

theory

mysql 支持 emoji 表情

mysql数据库默认用的是utf8编码,utf8编码存储时用的是三个字节,但Emoji表情是4个字节,所以导致了数据插入异常。

mysql5.5.3之后的版本支持 utf8mb4 编码,这种编码是 utf8 的升级版,用的是4个字节进行存储。所以需要做的就是把mysql字符集从utf8升级为utf8mb4就行了。

Golang 使用什么数据类型?

Golang 使用以下类型:

  • Method
  • Boolean
  • Numeric
  • String
  • Array
  • Slice
  • Struct
  • Pointer
  • Function
  • Interface
  • Map
  • Channel

Go 程序中的包是什么?

包 (pkg) 是 Go 工作区中包含 Go 源文件或其他包的目录。

源文件中的每个函数、变量和类型都存储在链接包中。每个 Go 源文件都属于一个包,该包在文件顶部使用以下命令声明:

package <packagename>

您可以使用以下方法导入和导出包以重用导出的函数或类型:

import <packagename>

Golang 的标准包是 fmt,其中包含格式化和打印功能,如 Println()。

Go 将浮点数转换为整数

package main

import "fmt"

func main() 
	i := 55           //int
	j := 67.8         //float64
	sum := i + int(j) //j is converted to int
	fmt.Println(sum)  // 122

什么是 Goroutine?你如何停止它?

一个 Goroutine 是一个函数或方法执行同时旁边其他任何够程采用了特殊的 Goroutine线程。

Goroutine 线程比标准线程更轻量级,大多数 Golang 程序同时使用数千个 Goroutine。

要创建 Goroutine,请go在函数声明之前添加关键字。

go f(x, y, z)

通过向 Goroutine 发送一个信号通道来停止它。

Goroutines 只能在被告知检查时响应信号,因此您需要在逻辑位置(例如 for 循环顶部)包含检查。

package main

func main() 
	quit := make(chan bool)
	go func() 
		for 
			select 
			case <-quit:
				return
			default:
				// …
			
		
	()
	// …
	quit <- true

如何在运行时检查变量类型?

类型开关是在运行时检查变量类型的最佳方式。

类型开关按类型而不是值来评估变量。

每个 Switch 至少包含一个case,用作条件语句,和一个default case,如果没有一个case 为真,则执行。

Go 两个接口之间可以存在什么关系?

如果两个接口有相同的方法列表,那么他们就是等价的,可以相互赋值。

如果接口 A 的方法列表是接口 B 的方法列表的自己,那么接口 B 可以赋值给接口 A。接口查询是否成功,要在运行期才能够确定。

Go 当中同步锁有什么特点?作用是什么

当一个Goroutine(协程)获得了Mutex后,

其他Gorouline(协程)就只能乖乖的等待,除非该gorouline释放了该MutexRWMutex在 读锁 占用的情况下,

会阻止写,

但不阻止读 RWMutex 在 写锁 占用情况下,会阻止任何其他goroutine(无论读和写)进来,

整个锁相当于由该goroutine独占同步锁的作用是保证资源在使用时的独有性,不会因为并发而导致数据错乱,保证系统的稳定性。

Go 语言当中 Channel(通道)有什么特点,需要注意什么?

如果给一个 nil 的 channel 发送数据,会造成永远阻塞如果从一个 nil 的 channel 中接收数据,也会造成永久阻塞给一个已经关闭的 channel 发送数据, 会引起 pannic从一个已经关闭的 channel 接收数据, 如果缓冲区中为空,则返回一个零值。

Go 语言当中 Channel 缓冲有什么特点?

无缓冲的 channel是同步的,而有缓冲的 channel 是非同步的。

Go 语言中 cap 函数可以作用于那些内容?

cap 函数在讲引用的问题中已经提到,可以作用于的类型有:

  • array(数组)
  • slice(切片)
  • channel(通道)

go convey 是什么?一般用来做什么?

go convey是一个支持golang的单元测试框架

go convey 能够自动监控文件修改并启动测试,并可以将测试结果实时输出到Web界面。

go convey提供了丰富的断言简化测试用例的编写。

Go 语言当中 new 的作用是什么?

new 的作用是初始化一个值类型的指针 new 函数是内建函数,函数定义:

func new(Type) *Type

  • 使用 new 函数来分配空间。
  • 传递给 new 函数的是一个类型,而不是一个值。
  • 返回值是指向这个新非配的地址的指针。

Go 语言中 make 的作用是什么?

make 的作用是为 slice, map or chan 的初始化,然后返回引用 make函数是内建函数,函数定义:

func make(Type, size IntegerType) Type

make(T, args) 函数的目的和 new(T) 不同仅仅用于创建 slice, map, channel 而且返回类西行是实例。

Printf(),Sprintf(),FprintF() 都是格式化输出,有什么不同?

虽然这三个函数,都是格式化输出,但是输出的目标不一样。

  • Printf是标准输出,一般是屏幕,也可以重定向。
  • Sprintf()是把格式化字符串输出到指定的字符串中。
  • Fprintf()是吧格式化字符串输出到文件中。

Go 语言当中数组和切片在传递的时候的区别是什么?

1、 数组是值传递。
2、切片是引用传递。

Go 语言是如何实现切片扩容的?

Go 中切片扩容的策略是这样的:

首先判断,如果新申请容量大于2倍的旧容量,最终容量就是新申请的容量。

否则判断,如果旧切片的长度小于1024,则最终容量就是旧容量的两倍。

否则判断,如果旧切片长度大于等于1024,则最终容量从旧容量开始循环增加原来的 1/4, 直到最终容量大于等于新申请的容量。

如果最终容量计算值溢出,则最终容量就是新申请容量。

Go 扩容前后的 Slice 是否相同?

情况一:
原数组还有容量可以扩容(实际容量没有填充完),这种情况下,扩容以后的数组还是指向原来的数组,对一个切片的操作可能影响多个指针指向相同地址的Slice。

情况二:
原来数组的容量已经达到了最大值,再想扩容, Go 默认会先开一片内存区域,把原来的值拷贝过来,然后再执行 append() 操作。这种情况丝毫不影响原数组。

要复制一个Slice,最好使用Copy函数。

Go 的参数传递、引用类型

Go语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝。因为拷贝的内容有时候是非引用类型(int、string、struct等这些),这样就在函数中就无法修改原内容数据;有的是引用类型(指针、map、slice、chan等这些),这样就可以修改原内容数据。

package main

import "fmt"

func test(arr []int) 
	arr[1] = 8888


func main() 
	src := []int1, 2, 3
	dst := make([]int, len(src))
	copy(dst, src)

	test(src)
	fmt.Println(src) // [1 8888 3]

Golang的引用类型包括 slice、map 和 channel

它们有复杂的内部结构,除了申请内存外,还需要初始化相关属性。

内置函数 new 计算类型大小,为其分配零值内存,返回指针。

而 make 会被编译器翻译成具体的创建函数,由其分配内存和初始化成员结构,返回对象而非指针。

go 中 new 和 make 的区别?

简单来说,new 只分配内存,make 用于初始化 slice、map 和 channel。

Golang Map 底层实现

Golang 中 map 的底层实现是一个散列表,因此实现map的过程实际上就是实现散表的过程。

在这个散列表中,主要出现的结构体有两个:

  • hmap(a header for a go map)
  • bmap(a bucket for a Go map)

通常叫其 bucket。

Golang Map 如何扩容

装载因子:count/2^B

触发条件:

1、装填因子是否大于6.5
2、overflow bucket 是否太多

解决方法:
1、双倍扩容:扩容采取了一种称为“渐进式”地方式,原有的 key 并不会一次性搬迁完毕,每次最多只会搬迁 2 个 bucket。

2、等量扩容:重新排列,极端情况下,重新排列也解决不了,map成了链表,性能大大降低,此时哈希种子hash0的设置,可以降低此类极端场景的发生。

Golang Map 查找

Go语言中map采用的是哈希查找表,由一个key通过哈希函数得到哈希值,64位系统中就生成一个64bit的哈希值,由这个哈希值将key对应到不同的桶(bucket)中,当有多个哈希映射到相同的的桶中时,使用链表解决哈希冲突。

key经过hash后共64位,根据hmap中B的值,计算它到底要落在哪个桶时,桶的数量为2^B,如B=5,那么用64位最后5位表示第几号桶,在用hash值的高8位确定在bucket中的存储位置,当前bmap中的bucket未找到,则查询对应的overflow bucket,对应位置有数据则对比完整的哈希值,确定是否是要查找的数据。

如果两个不同的key落在的同一个桶上,hash冲突使用链表法接近,遍历bucket中的key 如果当前处于map进行了扩容,处于数据搬移状态,则优先从 oldbuckets 查找。

介绍一下 Channel

Go语言中,不要通过共享内存来通信,而要通过通信来实现内存共享。

Go的CSP(Communicating Sequential Process)并发模型,中文可以叫做通信顺序进程,是通过 goroutine 和 channel 来实现的。

所以channel收发遵循先进先出FIFO,分为有缓存和无缓存,channel中大致有buffer(当缓冲区大小部位0时,是个ring buffer)、sendx和recvx收发的位置(ring buffer记录实现)、sendq、recvq当前channel因为缓冲区不足而阻塞的队列、使用双向链表存储、还有一个mutex锁控制并发、其他原属等。

Go 语言的 Channel 特性?

1、给一个 nil channel 发送数据,造成永远阻塞。
2、从一个 nil channel 接收数据,造成永远阻塞。
3、给一个已经关闭的 channel 发送数据,引起 panic。
4、从一个已经关闭的 channel 接收数据,如果缓冲区中为空,则返回一个零值。

5、无缓冲的 channel 是同步的,而有缓冲的 channel 是非同步的。

6、关闭一个 nil channel 将会发生 panic。

Channel 的 ring buffer 实现

channel 中使用了 ring buffer(环形缓冲区) 来缓存写入的数据。

ring buffer 有很多好处,而且非常适合用来实现 FIFO 式的固定长度队列。

在 channel 中,ring buffer 的实现如下:


hchan中有两个与buffer相关的变量:recvx和sendx。

其中sendx 表示 buffer中可写的 index,recvx表示 buffer 中可读的 index。

从recvx到sendx之间的元素,表示已正常存放入 buffer中的数据。

我们可以直接使用buf[recvx]来读取到队列的第一个元素,使用buf[sendx] = x来将元素放到队尾。

Go 并发编程

1、Mutex 几种状态

  • mutexLocked — 表示互斥锁的锁定状态;
  • mutexWoken — 表示从正常模式被从唤醒;
  • mutexStarving — 当前的互斥锁进入饥饿状态;
  • waitersCount — 当前互斥锁上等待的 Goroutine 个数;

2、Mutex 正常模式和饥饿模式

2.1 正常模式(非公平锁)

正常模式下,所有等待锁的goroutine按照FIFO(先进先出)顺序等待。

唤醒的goroutine不会直接拥有锁,而是会和新请求锁的goroutine竞争锁的拥有。

新请求锁的goroutine具有优势:

它正在CPU上执行,而且可能有好几个,所以刚刚唤醒的goroutine有很大可能在锁竞争中失败。

在这种情况下,这个被唤醒的goroutine会加入到等待队列的前面。

如果一个等待的goroutine超过1ms没有获取锁,那么它将会把锁转变为饥饿模式。

2.2 饥饿模式(公平锁)

为了解决了等待G队列的长尾问题。

饥饿模式下,直接由unlock把锁交给等待队列中排在第一位的G(队头),同时,饥饿模式下,新进来的G不会参与抢锁也不会进入自旋状态,会直接进入等待队列的尾部,这样很好的解决了老的g一直抢不到锁的场景。

饥饿模式的触发条件,当一个G等待锁时间超过1毫秒时,或者当前队列只剩下一个g的时候,Mutex切换到饥饿模式。

3 总结

对于两种模式,正常模式下的性能是最好的,goroutine可以连续多次获取锁,饥饿模式解决了取锁公平的问题,但是性能会下降,其实是性能和公平的一个平衡模式。

Mutex 允许自旋的条件

1、锁已被占用,并且锁不处于饥饿模式。
2、积累的自旋次数小于最大自旋次数(active_spin=4)。
3、cpu核数大于1。
4、有空闲的P。
5、当前goroutine所挂载的P下,本地待运行队列为空。

RWMutex 实现

通过记录 readerCount 读锁的数量来进行控制,当有一个写锁的时候,会将读锁数量设置为负数1<<30。

目的是让新进入的读锁等待写锁之后释放通知读锁。

同样的写锁也会等等待之前的读锁都释放完毕,才会开始进行后续的操作。

而等写锁释放完之后,会将值重新加上1<<30, 并通知刚才新进入的读锁(rw.readerSem),两者互相限制。

RWMutex 注意事项

RWMutex 是单写多读锁,该锁可以加多个读锁或者一个写锁。

读锁占用的情况下会阻止写,不会阻止读,多个 goroutine 可以同时获取读锁。

写锁会阻止其他 goroutine(无论读和写)进来,整个锁由该 goroutine 独占。

适用于读多写少的场景。

RWMutex 类型变量的零值是一个未锁定状态的互斥锁。

RWMutex 在首次被使用之后就不能再被拷贝。

RWMutex 的读锁或写锁在未锁定状态,解锁操作都会引发 panic。

RWMutex 的一个写锁 Lock 去锁定临界区的共享资源,如果临界区的共享资源已被(读锁或写锁)锁定,这个写锁操作的 goroutine 将被阻塞直到解锁。

RWMutex 的读锁不要用于递归调用,比较容易产生死锁。

RWMutex 的锁定状态与特定的 goroutine 没有关联。一个 goroutine 可以 RLock(Lock),另一个 goroutine 可以 RUnlock(Unlock)。

写锁被解锁后,所有因操作锁定读锁而被阻塞的 goroutine 会被唤醒,并都可以成功锁定读锁。

读锁被解锁后,在没有被其他读锁锁定的前提下,所有因操作锁定写锁而被阻塞的 goroutine,其中等待时间最长的一个 goroutine 会被唤醒。

Cond 是什么?

Cond实现了一种条件变量,可以使用在多个Reader等待共享资源ready的场景(如果只有一读一写,一个锁或者channel就搞定了)

每个 Cond 都会关联一个Lock(*sync.Mutex or *sync.RWMutex),当修改条件或者调用 Wait 方法时,必须加锁,保护 condition。

Broadcast 和 Signal 区别?

func (c *Cond) Broadcast()

Broadcast 会唤醒所有等待 c 的 goroutine。

调用 Broadcast 的时候,可以加锁,也可以不加锁。

func (c *Cond) Signal()

Signal 只唤醒 1 个等待 c 的 goroutine。

调用 Signal 的时候,可以加锁,也可以不加锁。

Cond 中 Wait 使用

func (c *Cond) Wait()

Wait() 会自动释放 c.L,并挂起调用者的goroutine。

之后恢复执行,Wait() 会在返回时对 c.L 加锁。

除非被 Signal 或者 Broadcast 唤醒,否则 Wait() 不会返回。

由于 Wait() 第一次恢复时,C.L并没有加锁,所以当Wait返回时,调用者通常并不能假设条件为真。

取而代之的是, 调用者应该在循环中调用Wait。
(简单来说,只要想使用condition,就必须加锁。)

c.L.Lock() 
for !condition()  
	c.Wait() 


... make use of condition ... 

c.L.Unlock()

WaitGroup 用法

一个 WaitGroup 对象可以等待一组协程结束。

使用方法是:

1、main协程通过调用 wg.Add(delta int) 设置worker协程的个数,然后创建worker协程;

2、worker协程执行结束以后,都要调用 wg.Done();

3、main协程调用 wg.Wait() 且被block,直到所有worker协程全部执行结束后返回。

WaitGroup 实现原理

WaitGroup主要维护了2个计数器,一个是请求计数器 v,一个是等待计数器 w,二者组成一个64bit的值,请求计数器占高32bit,等待计数器占低32bit。

每次Add执行,请求计数器v加1,Done方法执行,请求计数器减1,v为0时通过信号量唤醒Wait()。

什么是 sync.Once

Once 可以用来执行且仅仅执行一次动作,常常用于单例对象的初始化场景。

Once 常常用来初始化单例资源,或者并发访问只需初始化一次的共享资源,或者在测试的时候初始化一次测试资源。

sync.Once 只暴露了一个方法 Do,你可以多次调用 Do 方法,但是只有第一次调用 Do 方法时 f 参数才会执行,这里的 f 是一个无参数无返回值的函数。

什么是 CAS?

CAS的全称为Compare And Swap,直译就是比较交换。

是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值,其实现方式是给予硬件平台的汇编指令,在intel的CPU中,使用的cmpxchg指令,就是说CAS是靠硬件实现的,从而在硬件层面提升效率。

简述过程是这样:

假设包含3个参数内存位置(V)、预期原值(A)和新值(B)。

V表示要更新变量的值,E表示预期值,N表示新值。

仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程在做更新,则当前线程什么都不做,最后CAS返回当前V的真实值。

CAS操作时抱着乐观的态度进行的,它总是认为自己可以成功完成操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对于当前线程的干扰。

sync.Pool 有什么用?

对于很多需要重复分配、回收内存的地方,sync.Pool 是一个很好的选择。

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

Go Runtime

1、Goroutine 定义

Goroutine 是一个与其他 goroutines 并行运行在同一地址空间的 Go 函数或方法。

一个运行的程序由一个或更多个 goroutine 组成。

它与线程、协程、进程等不同。

它是一个 goroutine” —— Rob Pike

Goroutines 在同一个用户地址空间里并行独立执行 functions,channels 则用于 goroutines 间的通信和同步访问控制。

2、GMP 指的是什么

G(Goroutine):
我们所说的协程,为用户级的轻量级线程,每个Goroutine对象中的sched保存着其上下文信息。

M(Machine):
对内核级线程的封装,数量对应真实的CPU数。
(真正干活的对象)

P(Processor):
即为G和M的调度对象,用来调度G和M之间的关联关系,其数量可通过GOMAXPROCS()来设置,默认为核心数。

sysmon 有什么作用?

sysmon 也叫监控线程,变动的周期性检查,好处。

  • 释放闲置超过5分钟的 span 物理内存;
  • 如果超过2分钟没有垃圾回收,强制执行;
  • 将长时间未处理的 netpoll 添加到全局队列;
  • 向长时间运行的 G 任务发出抢占调度(超过10ms的g,会进行retake);
  • 收回因 syscall 长时间阻塞的 P;

三色标记原理

我们首先看一张图,大概就会对 三色标记法有一个大致的了解:


原理:首先把所有的对象都放到白色的集合中。

  • 从根节点开始遍历对象,遍历到的白色对象从白色集合中放到灰色集合中。
  • 遍历灰色集合中的对象,把灰色对象引用的白色集合的对象放入到灰色集合中,同时把遍历过的灰色集合中的对象放到黑色的集合中。
  • 循环步骤3,知道灰色集合中没有对象。
  • 步骤 4 结束后,白色集合中的对象就是不可达对象,也就是垃圾,进行回收。

以上是关于Golang M 2023 5 * theory的主要内容,如果未能解决你的问题,请参考以下文章

Golang M 2023 4 topic

Golang M 2023 7 * theory

Golang M 2023 6 topic

Golang basic_leamingM 2023 2 theory

面向校招全力备战2023Golang实习与校招

面向校招全力备战2023Golang实习与校招