系列综述:
💞目的:本系列是个人整理为了Go语言
学习的,整理期间苛求每个知识点,平衡理解简易度与深入程度。
🥰来源:材料主要源于Go语言趣学指南
进行的,每个知识点的修正和深入主要参考各平台大佬的文章,其中也可能含有少量的个人实验自证。
🤭结语:如果有帮到你的地方,就点个赞和关注一下呗,谢谢🎈🎄🌷!!!
🙌请先收藏,未完待续…
文章目录
- 零、概述
- 什么是Go语言
- 一、基本语法
- 二、Go的执行
- 概述
- 调度器scheduler
- Goroutine
- Processor
- Machine
- 监控
- 三、垃圾回收
- 概述
- 算法原理
- 语言比较
- 二、编译
- 参考博客
😊点此到文末惊喜↩︎
零、概述
什么是Go语言
- Go语言的特点
编译型
语言,执行效率高- 原生的
并发支持
,支持轻量级的携程和通信机制 - 具有
内存回收机制
,支持安全自动的管理内存
- 第一个Go程序
package main // 声明当前文件所属的包 import ( // 引入其他包供当前文件使用 "fmt" // fmt:用于格式化输入输出的包 ) // 注意 { 必须和func在同一行 func main() { // 定义一个名字为main的函数 // 变量和常量 var variable = 123; // 变量的声明 variable *= 2; // 运算符简写 variable++; // go中没有前置++ const k = 11; // 常量的声明 // 格式化打印 fmt.Println("hello world",1*2) fmt.Printf("%-15v : %4v\n", "SpaceX", 94) // %v为占位符,前面的正负数字表示占用的位数,不足使用空格填充 } // 函数定义 func swap(x, y string) (string, string) { return y, x }
- Go程序的起点
main包中的main函数
。当运行一个Go程序时,编译器会自动寻找main包,并执行其中的main函数。
- 常量和变量
一、基本语法
- Go函数中的多个返回值是哪几种
- 返回
值类型
,表示函数的执行结果 - 返回
error类型
,表示函数的执行成功与否的情况
- 返回
- Go中的数据类型类型
- 值类型
- 布尔类型(bool):表示
true/false
的值 - 整数类型(int)
- 有符号整型:包括
int, int8, int16, int32, int64
- 无符号整型:包括
uint, uint8,
uint16, uint32, uint64` - 指针无符号整型
uintptr
:用于和底层编程交互,常用于存储指针的整形表示
- 有符号整型:包括
- 浮点数类型:包括
单精度浮点数(float32)
和双精度浮点数(float64)
。 - 复数类型:由
实部和虚部
组成,有单精度复数(complex64)
和双精度复数(complex128)
。 - 字符串类型(string):表示一串字符。
- 字符类型(rune):表示一个Unicode字符。
- 数组类型(array):具有固定大小和相同类型的连续元素的集合。
- 结构体类型(struct):表示不同类型的字段组合。
- 布尔类型(bool):表示
- 引用类型
- 切片类型(slice)
- 定义:是一个动态数组结构,包含起始位置、长度和容量。
- 操作:超过容量会进行
扩容
,低于容量的 1 / 4 1/4 1/4会进行缩容
- 开销:slice是基于数组的申请和复制实现的动态性的,会有开销
- 映射类型(map):无序的键值对集合
- 函数类型(func):可以将函数看成一种类型,用于变量的声明
- 通道类型(channel):用于协程间安全同步的传送数据
- 切片类型(slice)
- 接口类型(interface):表示一组不实现的方法集合
- 错误类型(error):表示错误信息的
预定义的接口类型
- 错误类型(error):表示错误信息的
- 值类型
- 介绍一下
nil
- 表示类型声明的变量未被初始化或赋值,
- 注意
- 不同类型的nil进行比较需要通过相应的类型判断函数
- 如果指针类型未初始化,尝试对nil进行解引用会导致panic
- 介绍一下Go的异常类型
- 定义:预定义的error接口类型
- 作用:用error类型代替try…catch语句,节省资源,增加代码可读性
// error接口定义 type error interface { Error() string } // 错误示例 package main import ( "fmt" "errors" ) func divide(a, b int) (int, error) { //健壮性检查 if b == 0 { return 0, errors.New("division by zero") } return a / b, nil } func main() { result, err := divide(10, 2) // 注意类型的接收 if err != nil { fmt.Println("Error:", err) } else { fmt.Println("Result:", result) } }
二、Go的执行
概述
- Go程序的执行由两部分组成
- Program:用于处理用户输入和执行用户态下的相关操作
- Runtime:帮助用户程序处理与内核相关的系统调用,通过调度器
scheduler
提高执行效率
调度器scheduler
- 定义
- Go调度器通过
GPM
机制实现M:N
的调度模型,提高垃圾回收效率,实现高效的并发编程。
- Go调度器通过
- GPM机制
- Goroutine(G)代表Go语言的协程,是调度的基本单位
- Processor(P)代表执行Goroutine的上下文环境及资源,是中间调度器
- Machine(M)是通过
系统调用int 0x56
创建的内核线程的抽象 - 关系
- 每个Go程序只有一个GPM调度器schedt
- 每个M绑定一个内核线程
- M可以关联多个P:因为P维护了上下文,可以在M绑定的内核线程阻塞的情况下,切换到另一个M上执行
- P可以调度多个G:通过
每个P本地就绪的G队列
和所有P共享的全局G队列
实现
- 池化优化:每个G、P和M都有自己的
free队列
,用于存储空闲的G/P/M对象,用时直接取,释放时放回,避免频繁的拷贝和销毁开销 - P的数量:可通过runtime.GOMAXPROCS函数进行设定,默认为当前系统的CPU核数。
- M:N调度模型
- 原理:
N个协程G
通过调度器P
管理,从而映射到M个内核线程M
上运行 - 作用:平衡内核线程负载,充分提高系统资源的利用率
- 原理:
- 初始化执行过程
- 执行schedinit 函数:初始化调度器相关的参数
- 初始化 g0 栈:为运行 runtime 代码提供一个“环境”
- 主线程绑定并初始化m0
- 编译器将
go func() {}
翻译成newproc函数
- newproc:获取函数的参数和代码段地址,通过g0创建G
- newproc 函数获取了参数和当前g的pc信息,并通过g0调用newproc1去真正的执行创建或获取可用的g
- newporc1 的作用就是创建或者获取一个空间的g,初始化这个g,并通过gfget尝试寻找一个p和m去执行g
Goroutine
- 定义:Goroutine是一种
轻量级
的并发实现方式,使用管道机制(channle)
进行不同Goroutine间的通信,按照算法将goroutine分配到多个线程上执行,从同高效的利用多核处理器的并发性。 - Goroutine的组成
- 栈帧(BP:SP):用于保存函数调用相关信息
- 程序计数器(PC):指向当前正在执行的执行地址
- 执行状态(State):表示当前Goroutine的运行状态
- 其他基本属性信息,如抢占标记、链式指针、id等
- 特点
- 轻量级(协程与内核线程的区别)
- 动态小栈:内核线程栈空间通常2MB,而每个Goroutine的
初始栈空间只有2KB
,并能通过分段栈进行动态扩展
,以适应程序的需求。 - 上下文切换开销小:协程上下文切换只需在用户态下进行三个寄存器(PC、SP和BP)的切换。而线程上下文切换需要陷入内核,并进行16个寄存器的切换,大概5倍的性能开销。
- 动态小栈:内核线程栈空间通常2MB,而每个Goroutine的
- 管道通信(channel)
- 作用:channel是不同Goroutine间安全的数据传递同步机制
- 组成:环形队列缓冲区和读写等待队列
- 从channel读数据时,若channel缓冲区为空或没有缓冲区,则阻塞当前读线程,并加入到recvq队列中 。
- 向channel写数据时,若缓冲区已满或没有缓冲区,则阻塞当前写线程,加入sendq队列中
- M:N协程调度模型
- 作用:将M个用户态线程(协程)映射到N个的内核线程上运行的调度模型。从而充分利用多核处理器的性能,同时减少线程上下文切换的开销。
- 工作窃取(Work Stealing):当P的本地队列为空时,从其他P的本地队列偷取G运行
- 调度器退化(Scheduler Pacing):通过抢占式调度,调度器中断并切换到其他协程。从而避免长时间运行的任务(例如计算密集型的任务)导致其他协程的饥饿现象
- 轻量级(协程与内核线程的区别)
- goroutine的状态机模型
- 创建:Go 必须对每个运行着的线程上的 Goroutine 进行调度和管理。这个调度的功能被委托给了一个叫做 g0 的特殊的 goroutine, g0 是每个 OS 线程创建的第一个goroutine。
- 终止:在创建 goroutine 时,Go在开启实际go执行片段之前,通过PC寄存器设置了SP寄存器的首个函数栈帧(名为goexit的函数),这个技巧强制goroutine在结束工作后调用函数goexit。
Processor
- 定义:存储就绪状态的G队列,并调度和管理goroutine的执行,以实现高效的并发执行和资源利用
- P的相关队列
- RunQueue:每个P拥有一个存储
就绪状态(runnable)G
的队列,避免对锁竞争激烈的全局队列的直接依赖 - GlobalQueue:所有P共享同一个存储
就绪状态G
的队列,并由互斥锁进行并发访问的同步
- RunQueue:每个P拥有一个存储
- 作用
- 维护本地的就绪Goroutine队列,并通过调度算法选择合适的Goroutine到M上执行
- 管理资源:通过互斥锁同步对于共享资源的并发执行
- 协调和通信:p会与其他p进行协调和通信,比如在负载均衡时,负责将Goroutine调度到其他p上执行,以充分利用系统的资源。
- P的调度算法
- 新建的G会优先加入到P的本地队列。
- P的本地队列满了:则将本地队列中一半的G移动到全局队列
- M获取G的优先级
- 从P的
本地队列
获取Goruntine - 从P共享的
全局队列
获取Goruntine - 处理网络IO阻塞的
网络轮询器
获取可运行的G 其它的P的本地队列
窃取Goroutine
- 从P的
Machine
- 定义:是一个虚拟的执行环境,负责执行和管理Goroutine。
- 作用:
- 执行Goroutine
- 与内核线程进行交互,调度器根据系统负载动态创建或销毁M
- 若Goroutine 执行时发生系统调用和阻塞,M会将该G标记阻塞状态,并交于调度器进行处理
监控
- 定义:在启动阶段,创建一个独立的M执行sysmon函数,不需要依赖P
- 原理
- 调度器创建一个独立M执行sysmon函数
- 释放闲置超过5分钟的span物理内存
- 如果超过两分钟没有执行垃圾回收,则强制执行
- 将长时间未处理的netpoll结果添加到任务队列
- 向长时间运行的g进行抢占
- 收回因为syscall而长时间阻塞的p
- 监控线程首次休眠20us,每次执行完后,增加一倍的休眠时间,但是最多休眠10ms
- 调度器创建一个独立M执行sysmon函数
三、垃圾回收
概述
- 定义:垃圾回收(Garbage Collection,简称GC)是一种自动内存管理机制,
- 当开始垃圾回收时,运行时只需要等待当前正在CPU核上运行的那个Goroutine停止即可,而不需要等待所有的Goroutine。这样可以大大减少阻塞的时间,提高垃圾回收的效率。
- Go语言的GC流程(并发标记清除算法)
- 标记阶段(Marking Phase):
- 在标记阶段和再标记阶段,通过STW机制暂停所有的Go程来确保标记的准确性;
- 在清除阶段和内存整理阶段,采用并发方式来提高垃圾回收的效率。
- GC的触发条件
- 主动触发:通过调用 runtime.GC 来触发,但若有正在执行的GC,会阻塞等待
- 被动触发:
- 系统监控:当超过两分钟没有产生任何 GC 时,强制触发 GC。
- 步调算法:?
- 如果内存分配速度超过了标记清除的速度怎么办?
- 目前的 Go 实现中,当 GC 触发后,会首先进入并发标记的阶段。并发标记会设置一个标志,并在 mallocgc 调用时进行检查。当存在新的内存分配时,会暂停分配内存过快的那些 goroutine,并将其转去执行一些辅助标记(Mark Assist)的工作,从而达到放缓继续分配、辅助 GC 的标记工作的目的。
- 编译器会分析用户代码,并在需要分配内存的位置,将申请内存的操作翻译为 mallocgc 调用,而 mallocgc 的实现决定了标记辅助的实现,
- GC的性能指标
- CPU占用率:回收算法执行占用的CPU时间
- GC停顿的时间和频率:需要考虑 STW 和 Mark Assist 两个部分可能造成的停顿
- GC可扩展性:堆增大时,垃圾回收器的性能
- Go 的 GC 如何调优
- 对停顿敏感:由于GC的执行,导致用户代码执行的滞后。优化用户代码从而减少分配内存的数量
- 对资源消耗敏感:GC增加了对CPU的占用率
- 原则
- 避免过早优化,并只优化性能瓶颈
- 减少内存申请次数(降本)
- 提高内存申请速度(增效)
- 池化算法:预申请和复用提高申请速度
- Go 的垃圾回收器有哪些相关的 API?其作用分别是什么?
- runtime.GC:手动触发 G
- runtime.ReadMemStats:读取内存相关的统计信息,其中包含部分 GC 相关的统计信
- debug.FreeOSMemory:手动将内存归还给操作系
- debug.ReadGCStats:读取关于 GC 的相关统计信
- debug.SetGCPercent:设置 GOGC 调步变
- debug.SetMaxHeap(尚未发布[10]):设置 Go 程序堆的上限值
算法原理
Go语言中的垃圾回收(GC)算法采用了三色标记(tricolor marking)算法,基于并发标记清除(concurrent mark and sweep)策略。
三色标记算法将所有对象分为三个颜色:白色、灰色和黑色。
白色:表示对象尚未被访问和标记。
灰色:表示对象已经被访问,但是其引用尚未全部扫描。
黑色:表示对象已经被访问,并且其引用已经全部扫描。
GC 运行的步调由两个阶段组成:标记(marking)和清除(sweeping)。
标记阶段:
标记阶段以根对象(root object)为起点,递归地遍历所有可达对象,并将其标记为灰色。
将标记为灰色的对象标记为黑色,并将其引用对象标记为灰色。
重复上述过程,直到没有灰色对象为止。
清除阶段:
从堆中的所有对象中,找到没有标记为黑色的对象,即为垃圾对象。
将垃圾对象释放或回收,并将其空闲内存归还给堆。
Go语言中的GC算法是基于并发标记清除的,即在标记阶段和清除阶段都可以和用户程序同时进行。这样可以最大程度地减小GC对程序性能的影响,并且尽量保持内存的使用效率。
总结起来,Go语言中的垃圾回收(GC)采用了三色标记算法,通过并发标记清除策略进行垃圾对象的标记和释放。这种算法可以高效地管理内存,并且尽量减少对程序性能的影响。
语言比较
- 是否具有原生GC
- 原生GC:Python、Java、JavaScript、Objective-C、Swift
- 手动内存管理:C、C++、Rust
- 垃圾回收的优劣
- 优点:编程简单,无需程序员手动释放,减少内存泄漏等相关问题、
- 缺点:具有额外的性能开销,仍然可能存在内存泄漏问题
- Java的GC原理
- 分布式GC:将对象依据存活时间分配到不同的区域,每次回收只回收其中的一个区域。
- 操作
- 将堆分成年轻代、老年代和永久代,触发条件是用户的配置和实际代码行为的预测
- 年轻代收集周期:只对年轻代对象进行收集与清理
- 老年代收集周期:只对老年代对象进行收集与清理
- 混合式收集周期:同时对年轻代和老年代进行收集与清理
- 完整 GC 周期:完整的对整个堆进行收集与清理
- 目前 Go 语言的 GC 还存在哪些问题
- Mark Assist 停顿时间过长
- Sweep 停顿时间过长
- 由于 GC 算法的不正确性导致 GC 周期被迫重新执行
- 创建大量 Goroutine 后导致 GC 消耗更多的 CPU
二、编译
- 逃逸分析
- 定义:在编译原理中,分析指针动态范围的方法。
- 发生条件:当一个对象的指针被多个方法或线程引用
- 模糊化处理:Go语言的逃逸分析是编译器执行静态代码分析后,对内存管理进行的优化和简化,它可以决定一个变量是分配到堆还栈上。所以堆和栈的区别对程序员“模糊化“了
- 基本原则
- 函数返回对一个变量的引用
- 编译器会分析代码的特征和代码生命周期,Go中的变量只有在编译器可以证明在函数返回后不会再被引用的,才分配到栈上,其他情况下都是分配到堆上。
- 编译器通过分析代码来决定将变量分配到何处。
- 编译器会根据变量是否被外部引用来决定是否逃逸:
- 如果函数外部没有引用,则优先放到栈中;
- 如果函数外部存在引用,则必定放到堆中;
- 原因:栈的分配和执行比堆快,如果无法在栈上分配,则会逃逸到堆上。但是堆的分配会增加GC压力
- GoPath 的作用在于提供一个可以寻找 .go 源码的路径,包含
- src 存放源文件
- pkg 存放源文件编译后的库文件,后缀为 .a
- bin 则存放可执行文件。
- Go的编译过程
- 词法分析、语法分析
- 中间代码的生成与优化
- 目标代码的生成与优化
- 链接
🚩点此跳转到首行↩︎
参考博客
- 知乎goroutine解释
- 知乎goroutine原理
- 协程管道通信机制
- golang 源码学习之GMP (goroutine)
- 深入理解Go-goroutine的实现及Scheduler分析
- Go 程序员面试笔试宝典
- Golang并发编程-GPM协程调度模型原理及结构分析
- 待定引用