提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
本阶段回归对go的学习、重点有3个:1 对go的底层机制的学习,对源码进行阅读,2 对中间件如消息队列等进行学习,预期时间2周。
验收计划:1写博客对所学习的底层机制进行输出,2 用新学的知识对项目进行改造
一、学习经过
- 根据Java的学习经验,搜索面试题作为底层机制学习的入口,基础go面试题,进阶篇但讲的一般,通过对面试题的阅读,主要是具备一定的认识,以及提取出有价值的问题去针对性的学习。
- 在豆包进行基础了解,因为它的回答很散乱不系统,所以带着问题去deepseek问,最后再豆包问没了解的细节,主要是deepseek老是服务器繁忙,不然也不至于跳来跳去的。
二、学习总结
1 内存管理
1 内存划分
Go的内存分配采用多级结构,各司其职,按其功能特点可分为:
1、 mspan(内存跨度)
功能:管理内存的基本单元,为对象分配提供了空间,通常一个mspan中装的都是同一种类型(大小)的对象。它负责将连续的物理内存页抽象成一个个可以存储对象的单元,为上层的对象分配提供了基础。
特点:
- mspan 由一组连续的物理内存页(page)组成,每个页的大小通常为 8KB。
- 一个 mspan 通常只存储同一种大小的对象。这种设计可以减少内存碎片,提高内存利用率。
2、 mcache(线程本地缓存)
功能:每个逻辑处理器(P)绑定一个mcache,用于无锁分配小对象(通常≤32KB)。
特点:
-
按对象大小分为多个span(如67种size class),每个span管理固定大小的内存块。
-
分配时直接操作本地缓存,无需加锁,速度极快。
-
微小对象(<16B)可能合并到同一内存块以减少碎片。
3、 mcentral(中心缓存)
功能:全局的span池,按size class分类,当mcache空间不足时向mcentral申请。
特点:
-
每个size class对应一个mcentral,内部通过两个链表(nonempty和empty)管理span。
-
访问需加锁,但通过分离链表减少竞争(如Go 1.14优化为无锁队列)。
4、 mheap(全局堆)
功能:管理大对象(>32KB)和向操作系统申请内存(如通过mmap)。
特点:
-
使用arena(go1.21引入的一种为堆内存进行分块的改进策略)组织虚拟内存,通过位图管理分配状态。
-
大对象直接从mheap分配,绕过mcache和mcentral。
-
全局锁保护,但通过细粒度锁(如每个arena独立锁)减少竞争。
5、 页分配器(Page Allocator)
功能:管理物理页的分配与回收,支持高效查找连续空闲页。
特点:
- Go 1.14后改用基数树(Radix Tree)结构,替代原有链表,提升查找速度。
- 支持快速分配和释放,减少GC停顿。
2 内存分配
- 小对象:mcache → 若无可用span → mcentral(加锁) → 若仍不足 → mheap(加锁) → 操作系统。
- 大对象:直接通过mheap分配,可能触发GC清理或向OS申请新内存。
3 内存回收
Go 语言的内存回收机制主要依靠垃圾回收器(Garbage Collector,简称 GC)来实现,其目的是自动回收不再使用的内存,以避免内存泄漏并提高内存的利用率。
垃圾回收器采用了标记 - 清除算法的改进版本,即三色标记法(Tri - Color Marking),并结合了写屏障(Write Barrier)技术,实现了并发的垃圾回收,减少了程序的停顿时间(Stop The World,简称 STW)。
三色标记法
白色对象:表示尚未被垃圾回收器访问到的对象,初始时所有对象都被标记为白色。
灰色对象:表示已经被垃圾回收器访问到,但它的部分引用还未被扫描的对象。
黑色对象:表示已经被垃圾回收器访问到,并且它的所有引用都已经被扫描过的对象,黑色对象不会再被重新扫描。
写屏障
写屏障的实现原理基于对对象引用修改操作的拦截。当程序进行对象引用修改时,写屏障会插入额外的代码来记录这个修改。这些记录信息会帮助垃圾回收器在后续的标记过程中正确处理对象的可达性。
工作流程
Go 语言的垃圾回收过程主要分为四个阶段:
- 标记准备(STW)
- 暂停所有的 goroutine,进行一些初始化工作,如标记根对象(全局变量、栈上的对象等),并将它们标记为灰色。
- 并发标记
- 恢复所有的 goroutine 继续执行,垃圾回收器和程序并发运行。
- 垃圾回收器从灰色对象开始,递归地扫描它们的引用,将被引用的白色对象标记为灰色,同时将扫描过的灰色对象标记为黑色。
- 在这个过程中,写屏障会记录对象引用的修改,确保标记的正确性。
- 标记终止(STW)
- 再次暂停所有的 goroutine,完成标记工作,确保所有可达对象都被标记为黑色,所有不可达对象都为白色。
- 并发清除
- 恢复所有的 goroutine 继续执行,垃圾回收器和程序并发运行。
- 垃圾回收器遍历所有的内存块,将白色对象占用的内存标记为空闲,以便后续的内存分配使用。
触发时机
- 内存分配阈值
当程序的内存分配达到一定阈值时,会触发垃圾回收。这个阈值是动态调整的,根据之前的垃圾回收情况和内存使用情况进行计算。 - 定时触发
垃圾回收器会定期检查内存使用情况,如果发现内存使用过高,会触发垃圾回收。 - 手动触发
开发者可以通过调用 runtime.GC() 函数来手动触发垃圾回收。
2 并发处理
go的并发实现有两个最大的特点,GMP调度模型 和 基于通信的并发原语
GMP调度模型
G(Goroutine):轻量级用户态线程,初始栈大小2KB(可动态扩缩),通过 go 关键字创建。
M(Machine):操作系统线程(内核线程),由操作系统调度,直接执行机器指令。
P(Processor):逻辑处理器,绑定M并管理G的运行队列(本地队列),数量由 GOMAXPROCS(系统中可同时执行用户级 Go 代码的操作系统线程的最大数量) 决定。
调度流程
- 启动阶段
程序启动时,Go 运行时会创建一定数量的 M 和 P。P 的数量默认等于 CPU 的核心数,可以通过 runtime.GOMAXPROCS 函数进行调整。同时,会创建一个初始的 Goroutine(即 main 函数所在的 Goroutine)。 - 调度过程
- 本地队列调度(无锁):每个 P 都有一个本地运行队列,用于存放待执行的 G(最多256)。当一个 M 获取到一个 P 后,会从该 P 的本地运行队列中取出一个 G 并执行。
- 全局队列调度(有锁):如果 P 的本地运行队列为空,M 会尝试从全局运行队列中获取 G。全局运行队列是所有 P 共享的,存放着新创建的或者被其他 P 转移过来的 G。
- 线程复用:当M因系统调用阻塞时,P会与M解绑并寻找空闲M或创建新M,避免线程浪费。
- 偷取调度:如果全局运行队列也为空,M 会从其他 P 的本地运行队列中 “偷取” 一半的 G 到自己的 P 的本地运行队列中执行,这种机制称为工作窃取(Work Stealing),可以提高 CPU 的利用率。
- 调度触发时机
- 主动让出:runtime.Gosched() 或 channel 阻塞时。
- 系统调用:如文件I/O、网络I/O导致M阻塞。
- 时间片耗尽:通过 sysmon 监控线程(每20μs检查一次),强制抢占长时间运行的G。
基于通信的并发原语
Go 语言倡导 “不要通过共享内存来通信,而要通过通信来共享内存”,基于此理念设计了强大的基于通信的并发元语,主要是 goroutine 和 channel。
goroutine数据结构
定义在 src/runtime/runtime2.go文件中,说下字段:
// 每个 goroutine 唯一的 ID
goid int64
// goroutine 的状态,有 Gidle、Grunning、Gwaiting 等多种状态
status uint32
// goroutine 的调度栈的状态
schedlink guintptr
// 表示是否需要抢占该 goroutine
preempt bool
// 标记该 goroutine 是否处于系统调用中
syscallsp uintptr
// 系统调用的返回值
syscallpc uintptr
// 表示是否在系统调用中被阻塞
sysblocktraced bool
// 系统调用阻塞的时间
sysblocktime int64
// 栈的信息,包含栈的起始和结束地址
stack stack
// 栈保护值,用于检测栈溢出
stackguard0 uintptr
// 栈保护值,在某些特殊情况下使用
stackguard1 uintptr
// 栈的扫描边界
stackscan uintptr
// 栈的大小
stkobjsize uintptr
// 调度信息,如程序计数器、栈指针等
sched gobuf
// 指向当前执行该 goroutine 的 m(操作系统线程)
m *m
// 指向当前关联的 p(处理器)
p puintptr
// 创建该 goroutine 的程序计数器
gopc uintptr
// 该 goroutine 所属的用户栈的指针
startpc uintptr
// 等待原因,如等待 channel 操作、锁操作等
waitreason waitReason
// 进入等待状态的时间
waitsince int64
// 指向等待的对象,如 channel
waitnote note
// 被该 goroutine 锁定的 m
lockedm *m
// 垃圾回收标记状态
gcing bool
// 栈扫描状态
scanstatus uint8
// 用于栈扫描的标记
schedlink guintptr
// 协程组的 ID
curg *g
// 协程组的前驱
goprev guintptr
// 用于 trace 机制的字段
traceseq uint64
// 用于 trace 机制的字段
traceback uint64
// 用于 trace 机制的字段
tracep puintptr
它的调度策略可以看前面的GMP调度模型
channel数据结构
// 环形缓冲区的数据指针
buf unsafe.Pointer
// 缓冲区的元素数量
qcount uint
// 缓冲区的容量
dataqsiz uint
// 发送操作的索引
sendx uint
// 接收操作的索引
recvx uint
// 等待接收的 goroutine 队列
recvq waitq
// 等待发送的 goroutine 队列
sendq waitq
// 用于保护 hchan 结构体的锁
lock mutex
// 元素的类型信息
elemtype *_type
// 元素的大小
elemsize uint16
// 关闭状态标记
closed uint32
// 指向元素的指针掩码
elemptrmask uintptr
// 用于调试的状态标记
debugChan uintptr
3基本数据类型的底层实现
4异常机制
参考这篇博客
panic的实现原理
当程序执行到panic函数时,它会停止当前的执行流程,并且开始执行所有被注册的defer语句。然后,程序会停止执行,并且将控制权交给调用栈中的上一级函数。如果调用栈中的任何函数都没有捕获这个异常,程序将会崩溃并且打印出错误信息。
在底层实现中,panic函数会创建一个panic结构体,并且将该结构体的指针保存在当前的goroutine结构体中。然后,panic函数会继续执行所有被注册的defer语句,直到所有的defer语句都执行完毕。最后,panic函数会停止执行,并且将控制权交给调用栈中的上一级函数。
recover的实现原理
当程序执行到recover函数时,它会检查当前的goroutine结构体中是否存在一个panic结构体的指针。如果存在,recover函数会返回该panic结构体的值,并且清除该结构体的指针。然后,程序会继续执行。
在底层实现中,recover函数会检查当前的goroutine结构体中是否存在一个panic结构体的指针。如果存在,recover函数会返回该panic结构体的值,并且清除该结构体的指针。如果不存在,recover函数会返回nil。
5 select底层原理
6 锁机制和sync包
go的并发主要还是通过管道通信实现,锁对比java少得多就只有互斥锁和读写锁两种
可以看看这篇博客进行了解
sync/atomic包尽量不要使用,它的主要操作包括swap操作、compare-and-swap操作、add操作、load操作、store操作,是通过硬件层面的操作和一些算法来保证数据的一致性和操作的不可中断
简单了解看这个
sync包除了两个锁还包含了WaitGroup、Once、Map、Pool和Cond,可以看这个链接进行了解
其中Map讲的不是很清楚单独说一下:
先来看数据结构:
type Map struct {
mu Mutex
read atomic.Value // readOnly
dirty map[interface{}]*entry
misses int
}
type readOnly struct {
m map[interface{}]*entry
amended bool // true if the dirty map contains some key not in m.
}
type entry struct {
p unsafe.Pointer // *interface{}
}
- mu: 一个互斥锁
- read:类型为 atomic.Value,存储的是 readOnly 结构体。readOnly 结构体中有一个 map 类型的 m 字段,它可以通过原子操作进行读取,所以读取操作不需要加锁。
- dirty:是一个普通的 map,包含了一些 read 中没有的键值对。当对 sync.Map 进行写操作时,会优先更新 dirty。
- misses:记录了从 read 中查找键但未找到的次数。当 misses 达到一定数量时,会将 dirty 提升为 read。
读操作
当调用 sync.Map 的 Load 方法读取键值对时,首先会尝试从 read 中查找。若找到,则直接返回结果,这个过程无需加锁。如果在 read 中未找到且 readOnly.amended 为 true,意味着 dirty 中可能存在该键值对,此时会加锁并从 dirty 中查找。
写操作
调用 Store 方法写入键值对时,会先尝试更新 read 中的键值对。如果键不在 read 中且 readOnly.amended 为 false,会加锁并将键值对写入 dirty 中,同时设置 readOnly.amended 为 true。
7 os包
先说明os包主要是对文件的控制,这个包太大了,封装了一堆系统调用,有兴趣详细了解的看这个,我个人倾向于将它当作工具,需要的时候看看找找,了解基本操作看这个。
总结
这一阶段准备分成多个部分,第一部分主要丰富对go的认识,后续看到了别的可能会再补,下一部分主要是了解go的一些集成中间件。