map
● map 中,key 的数据类型必须为可比较的类型,chan、map、func不可比较
底层原理
● map是一种用于存储键值对的数据结构,它提供了高效的查找和插入操作。map的底层实现原理是哈希表(Hash Table)。
● 哈希表是一种基于数组的数据结构,当我们创建一个map并向其中插入键值对时,Go语言会通过将键传递给哈希函数来计算得到哈希值,并确定该键值对在哈希表中的存储位置。
● Go语言会将哈希值作为索引,在内部数组中找到对应的存储桶(bucket)。每个存储桶可以存储一个或多个键值对。当多个键值对的哈希值相同时,它们会被存储在同一个存储桶中,形成一个链表。
● 在进行查找操作时,Go语言会计算给定键的哈希值,并根据哈希值找到对应的存储桶。然后,它会在存储桶中遍历链表,直到找到匹配的键或遍历完所有键值对。
● 这种哈希表的实现方式使得map在平均情况下具有常数时间复杂度(O(1)),即使在存在大量键值对的情况下,查找和插入操作的性能也能保持稳定。
● 由于哈希表的特性,map中的键是无序的。
哈希冲突
● 哈希冲突指的是不同的键具有相同的哈希值,即它们在哈希表内部的存储位置相同。当发生哈希冲突时,存储桶中会形成一个桶链表,用于存储具有相同哈希值的键值对。
● 解决
○ 链表法:拉链法中,将命中同一个桶的元素通过链表的形式进行链接,因此很便于动态扩展.(如果是相同位置,就把所有的值放在一个链表上)
○ 开放寻址法:开放寻址法中,在插入新条目时,会基于一定的探测策略持续寻找,直到找到一个可用于存放数据的空位为止.(如果相同位置上已有值,就在该位置后找一个空位放上后来的值)
map 扩容机制:
● 增量扩容:当桶内 key-value 总数/桶数组长度 > 6.5 时发生增量扩容,桶数组长度增长为原值的两倍;
● 等量扩容:当桶内溢出桶数量大于等于 2^B 时( B 为桶数组长度的指数,B 最大取 15),发生等量扩容,桶的长度保持为原值;
● 采用渐进扩容的方式,当桶被实际操作到时,由使用者负责完成数据迁移,避免因为一次性的全量数据迁移引发性能抖动.
struct
匿名结构体
● 没有名字直接定义并赋值
● struct{}{}
○ 用作set集合: set = make(map[string]struct{}),使用:set[“haha”] = struct{}{}
匿名字段 和 嵌套结构体
● 都是用于继承
● 方法可以重载
实现接口:结构体实现了一个接口的所有方法,就是实现了这个接口
slice
切片和数组的区别
● 数组和切片都是用来存储一组相同类型的元素的数据结构
● 内存分配方式不同:数组是连续的内存块,而切片则是动态分配的内存块。
● 长度不可变:数组的长度是固定的,一旦创建就不能改变;而切片的长度可以动态改变。
● 访问效率不同:由于数组是连续的内存块,所以访问数组中的元素比访问切片中的元素更快。而且,由于切片需要动态分配内存,所以在某些情况下可能会比数组更慢。
底层原理
● 切片的底层数据结构由三个部分组成:指针:指向底层数组第⼀个元素的指针、长度、容量
扩容规则
● 期望的容量如果⽐现有的⼆倍还⼤,那么newcap直接等于期望的容量。
● 如果期望容量在原容量2倍范围内,且原容量⼩于256,那么newcap就是原容量2倍。
● 如果原容量超了256,那么就循环每次增加(1.25x+192)
扩容之后
● 将原来的元素复制到新的底层数组中。
● 将原来的底层数组释放,使其可以被垃圾回收器回收。
● 更新slice 的ptr 和cap 字段,使其指向新的底层数组。
channal
定义
● 通道是在 Go 语言中用于协程(Goroutine)之间进行通信和同步的一种机制。它可以让不同的 Goroutine 之间安全地传递数据,实现并发编程的目标。
构造器函数
● 判断申请内存空间大小是否越界,mem 大小为 element 类型大小与 element 个数相乘后得到,仅当无缓冲型 channel 时,因个数为 0 导致大小为 0;
● 根据类型,初始 channel,分为 无缓冲型、有缓冲元素为 struct 型、有缓冲元素为 pointer 型 channel;
● 倘若为无缓冲型,则仅申请一个大小为默认值 96 的空间;
● 如若有缓冲的 struct 型,则一次性分配好 96 + mem 大小的空间,并且调整 chan 的 buf 指向 mem 的起始位置;
● 倘若为有缓冲的 pointer 型,则分别申请 chan 和 buf 的空间,两者无需连续;
● 对 channel 的其余字段进行初始化,包括元素类型大小、元素类型、容量以及锁的初始化.
写流程
两类异常情况处理
● 对于未初始化(var ch chan int = nil)的 chan,写入操作会引发死锁;
● 对于已关闭的 chan,写入操作会引发 panic.
写的具体流程
● 写之前的每一步先加锁
● 写时存在阻塞读协程:直接将写入的元素拷贝交给正在阻塞的读协程
● 写时无阻塞读协程但环形缓冲区仍有空间:将当前元素添加到环形缓冲区中对应的空位;解锁
● 写时无阻塞读协程且环形缓冲区无空间(因为此时没有读的g):
○ 1、把该写协程添加到当前 channel 的阻塞写协程队列中;
○ 2、此时等待读协程的到来,如果有读协程(该读协程会把阻塞的写协程对应的元素取走),该写协程就会被唤醒,只需回收写协程的资源。
● 每一步的最后都有:解锁,返回。
读流程
两类异常情况处理
● 对于未初始化(var ch chan int = nil)的 chan,读操作会引发死锁;
● channel 已关闭且内部无元素:val,ok := <- ch,此时的ok就为false,val是对应类型的0值
读的具体流程
● 读之前的每一步先加锁
● 读时有阻塞的写协程
○ 从阻塞写协程队列中获取到一个写协程;
○ 倘若 channel 无缓冲区,则直接读取写协程元素,并唤醒写协程;
○ 倘若 channel 有缓冲区,则读取缓冲区头部元素,并将写协程元素写入缓冲区尾部后唤醒写写成;
● 读时无阻塞写协程且缓冲区有元素:获取缓冲数组队首位置的元素,如果该元素是最后一个元素,把读指针调整到队首;
● 读时无阻塞写协程且缓冲区无元素:
○ 1、把 该读协程 添加到当前 channel 的阻塞读协程队列中;
○ 2、此时等待写协程的出现,如果写协程出现(该写协程会把阻塞的读协程对应的元素取走),该读协程就会被唤醒,只需回收读协程的资源。
● 每一步的最后都有:解锁,返回。
关闭 channel
● 关闭未初始化过的 channel 会 panic;
● 加锁;
● 重复关闭 channel 会 panic;
● 关闭channel后 ,阻塞写协程会panic,阻塞读协程依然可以读出来,但是两者不可能同时存在。
select多路复用:
select{
case <- ch :
// do some logic
case <- ch1:
//do some logic
default:
//do some logic
}
GMP模型调度器
GMP模型是Go语言调度器的底层原理,用于管理和调度goroutine的执行。它采用了一种类似于操作系统的多线程模型。
G
● G是Go语言中轻量级线程(goroutine)的抽象,它表示一个可执行的任务。
● G是由Go语言运行时(runtime)创建和管理的,它存储了goroutine的上下文信息,如栈、指令指针等。
● g需要绑定到 p 才能执行,在 g 的视角中,p 就是它的 cpu.
● Goroutine的创建和销毁是相对廉价的,Go语言可以创建成千上万个goroutine。
M
● m 即 machine,是 golang 中对线程的抽象;
● m 不直接执行 g,而是先和 p 绑定,由其实现代理;
● 借由 p 的存在,m 无需和 g 绑死,也无需记录 g 的状态信息,因此 g 在全生命周期中可以实现跨 m 执行.
P
● p 即 processor,是 golang 中的调度器;
● p 是 gmp 的中枢,借由 p 承上启下,实现 g 和 m 之间的动态有机结合;
● 对 g 而言,p 是其 cpu,g 只有被 p 调度,才得以执行;
● 对 m 而言,p 是其执行代理,为其提供必要信息的同时(可执行的 g、内存分配情况等),并隐藏了繁杂的调度细节;
● p 的数量决定了 g 最大并行数量,可由用户通过 GOMAXPROCS 进行设定(超过 CPU 核数时无意义)
基本流程
● M 是线程的抽象;G 是 goroutine;P 是承上启下的调度器;
● M调度G前,需要和P绑定;
● 全局有多个M和多个P,但同时并行的G的最大数量等于P的数量;
● G的存放队列有三类:P的本地队列;全局队列;和wait队列(图中未展示,为io阻塞就绪态goroutine队列);
● M调度G时,优先取P本地队列,其次取全局队列,最后取wait队列;这样的好处是,取本地队列时,可以接近于无锁化,减少全局锁竞争;
● 为防止不同P的闲忙差异过大,设立work-stealing机制,本地队列为空的P可以尝试从其他P本地队列偷取一半的G补充到自身队列.
GMP调度流程
● g被分为两类
○ I 负责调度普通 g 的 g0,执行固定的调度流程,与 m 的关系为一对一;
○ II 负责执行用户函数的普通 g.
● g0 执行 schedule() 函数,寻找到用于执行的 g;
● g0 执行 execute() 方法,更新当前 g、p 的状态信息,并调用 gogo() 方法,将执行权交给 g;
● g 因主动让渡( gosche_m() )、被动调度( park_m() )、正常结束( goexit0() )等原因,调用 m_call 函数,执行权重新回到 g0 手中;
● g0 执行 schedule() 函数,开启新一轮循环.
调度类型
● 主动调度:一种用户主动执行让渡的方式,主要方式是,用户在执行代码中调用了 runtime.Gosched 方法,此时当前 g 会当让出执行权,主动进行队列等待下次被调度执行.
● 被动调度:因当前不满足某种执行条件,g 可能会陷入阻塞态无法被调度,直到关注的条件达成后,g才从阻塞中被唤醒,重新进入可执行队列等待被调度.
● 正常调度:g 中的执行任务已完成,g0 会将当前 g 置为死亡状态,发起新一轮调度.
● 抢占调度:倘若 g 执行系统调用超过指定的时长,且全局的 p 资源比较紧缺,此时将 p 和 g 解绑,抢占出来用于其他 g 的调度. 等 g 完成系统调用后,会重新进入可执行队列中等待被调度.
gc垃圾回收
基础
● 核心:分配在堆上的内存不会再使用时,Go语言将会自动回收分配在堆上的内存,从而避免系统的内存被占满。垃圾回收的核⼼就是把没有被引⽤的内存回收掉,以供后续内存分配时使⽤。
● Go语言中GC思想:Go中采用最简单的“标记-清除”,“标记有用的对象,清除无用的对象”,采用广度优先搜索算法,从根集合出发,进行可达性分析,标记有用对象。
● go中采⽤的是三⾊标记法来处理
串行GC
● 对于垃圾回收来说,回收过程中也需要控制住内存的变化,否则回收过程中指针传递,会引起内存引⽤关系变化,如果错误的回收了还在使⽤的内存,就会发⽣其他问题。
● 缺点:Golang中的STW(Stop The World)就是停掉所有的goroutine,专⼼做垃圾回收,待垃圾回收结束后再恢复goroutine
● STW时间的太长会影响应⽤的执⾏,且对性能的影响较大,难以支持高并发。
并发GC
为了减少GC对性能的影响,Go现在的版本支持并发进行垃圾回收。采用的标记法为三色标记法。
三色标记法具体过程
● ⾸先把所有的对象都放到⽩⾊的集合中
● 从根节点开始遍历对象,遍历到的⽩⾊对象从⽩⾊集合中放到灰⾊集合中
● 遍历灰⾊集合中的对象,把灰⾊对象引⽤的⽩⾊集合的对象放⼊到灰⾊集合中,同时把遍历过的灰⾊集合中的 对象放到⿊⾊的集合中
● 循环步骤3,直到灰⾊集合中没有对象
● 步骤4 结束后,⽩⾊集合中的对象就是不可达对象,也就是垃圾,进⾏回收
标记的不足
三色标记法主要用于解决并发GC问题,但是如果只有三色标记法不做其他处理的话,在进行GC的过程中如果有元素的删除,或者元素的插入,或者元素的指向关系发生变化的时候,可能会出现误清除对象的问题。解决办法:在进行增删改的时候加入屏障。
加入写屏障
● 写屏障类似⼀种开关,在GC的特定时机开启,开启后发生以上操作时(增删改),本轮不回收相应元素,下次GC时再确定。
垃圾回收触发时机
● 每次内存分配时都会检查当前内存分配量是否已达到阀值,如果达到阀值则⽴即启动 GC。阀值= 上次GC内存分配量* 内存增长率。内存增长率由环境变量GOGC控制,默认为100,即每当内存扩⼤⼀倍时启动GC。
● 默认情况下,最长2分钟触发⼀次GC。也可以进⾏⼿动触发,程序代码中也可以使⽤runtime.GC()来⼿动触发GC。
内存逃逸
定义
Go语言中的内存逃逸指的是在函数中分配的变量无法在函数返回后被立即回收,从而导致在堆上分配内存,造成了额外的内存开销和GC压力。内存逃逸的原因可以是变量被分配到函数的堆栈帧之外,或者被分配到包含指针的数据结构中。简单来说,局部变量通过堆分配和回收,就叫内存逃逸 。
内存逃逸的情况分析
● 1、不确定数据类型,fmt.println(a),因为Println(a.interfacel)的参数是 interface{} 类型,编译期无法确定其具体的参数类型,所以内存分配到堆中。
● 2、指针的引用,一个函数返回的是一个变量的指针类型,即变量在函数外部存在引用,这里避免非法内存(就是地址还在,内存被gc释放)的存在,所以必须将指针的内存分配到堆上
● 3、内存较大,遍历一万次往一个一万长度的切片里赋值,这会将切边的这个变量放到堆上,如果减少遍历的次数,该变量就会放在栈上
● 4、变量大小不确定,l := 1, a:= make([]int,l,l),这里的a变量大小就是不确定的。因为 l 可能在后续的代码中被修改,所以编译器认为应该分配到堆上。如果改成确定的数值,就会被分配到栈上。
分析内存逃逸可以帮助我们:
● 1、优化程序性能(导致程序占用过多的内存资源)
● 2、防止内存溢出(导致程序崩溃)
● 3、提高代码质量
在Go语言中,使用go build -gcflags "-m"命令可以查看代码中的内存逃逸情况。如果有变量逃逸到堆上,编译器会输出相应的警告信息,以帮助开发者识别和优化代码。