《 九 阴 真 经 卷 一 》Golang

《 九 阴 真 经 卷 一 》Golang 《 九 阴 真 经 卷 一 》Golang文章目录《 九 阴 真 经 卷 一 》Golang一. Go语言的原语、关键字、语言特性1. make、new2. range3. defer、recover4. rune类型5. select6. init函数7. interface与类型断言8. 闭包二、Go的三种引用类型1. Slice多个切片何时会指向同一个数组2. Map什么类型不能作为Map的Key3. Channel三、 Go的内置包1. Context2. Sync atomic3. Sort4. Reflect四、内存相关1. 虚拟内存2. 内存管理策略3. 内存对齐4. 内存逃逸5. GC五、goroutine1. 进程、线程与协程的区别2. GMP模型3. goroutine池的实现4. goroutine六、Go运行时1. Go运行时负责什么2. Go运行时是如何被执行的七、性能分析pprof可以查看的资源一. Go语言的原语、关键字、语言特性1. make、newmake与new的区别相同点make和new都是在堆上分配内存不同点make仅能用于三种引用类型new会将开辟的内存区域清零make不是简单的清零而是初始化底层数据结构make返回的是引用类型对象本身new返回的是指针2. rangerange时局部变量的生存周期range中的局部变量随着range循环地址不会发生变化3. defer、recover底层实现defer与panic的底层实现都是链表链表头的指针存储在goroutine结构体中recover的实现是修改panic结构体将其中代表是否恢复的字段设置为true多个defer的执行顺序多个defer的执行顺序类似栈先进后出defer与return的执行顺序return先执行defer后执行defer与具名返回值具名返回值会在函数开始时自动初始化作用域为整个函数defer可以修改具名返回值佐证了defer的执行时机晚于return同理函数返回指针指向的内容也可以被defer修改在defer之前panic会如何已经执行到的defer即使后续出现了panic也会执行defer中的内容利用这个特性可以在defer中完成一些关闭资源的操作避免资源泄露如果代码没有执行到defer那一行则无法执行该defer多个recover会如何panic只会被最后一个recover捕获参考https://static.kancloud.cn/aceld/golang/1958310参考https://www.bilibili.com/video/BV155411Y7XT/?spm_id_from333.999.0.04. rune类型Go的默认编码类型是utf-8介绍一下rune类型rune等同于int32类型用于处理unicode或utf-8字符byte等同于int8类型用于处理ascii字符单引号’’ 双引号 反引号的区别单引号表示byte类型或rune类型即一个字符双引号表示字符串即字符数组反引号表示字符串且不支持转义5. selectselect的作用用多个case语句监视多个channel类似多路复用有default分支的select就不会阻塞select的实现select的实现在runtime.selectgo()用一个数组储存所有case分支顺序为send在前recv在后。在轮询这些case时是以随机打乱的顺序进行的以保障公平性。所以会有一个长度是case数组两倍的数组用以保存轮询顺序(保证公平)和加锁顺序(防止死锁)。select的工作原理是轮询所有case在轮询前会加锁轮询时检查channel的等待队列和缓冲区如果有channel可以操作就进入对应的case分支如果没有就将当前协程加入到channel的发送和接收的等待队列中然后释放锁等待channel变为可操作状态。当有channel可操作时也会按加锁顺序把所有channel上锁从等待队列中移除当前的协程再释放锁。参考https://www.bilibili.com/video/BV1hv411x7we?p29vd_sourced0c878ce37e7bc95bc121f75c62a5fc36. init函数不同包的init函数执行顺序从被导入的最深层的包开始执行直至main包每个包的init函数只会执行一次同一个包的多个文件的init函数执行顺序按文件名顺序执行init函数和包级别变量初始化的先后包级别变量的初始化先于init函数7. interface与类型断言空接口和非空接口各自的实现空接口的实现空接口的结构体为eface主要字段为接口的动态类型是一个类型元数据的指针和一个unsafe.Pointer指向接口的动态值。赋值之前这两个字段都为nil非空接口的实现非空接口的结构体为iface主要字段为一个接口动态值指针和一个指向itabitab结构体中会存放接口需要的方法列表、动态类型元数据和从类型元数据中拷贝而来的接口所需方法实现的地址这样在使用接口调用方法的时候就不需要查询类型元数据。特殊的itab结构体代表接口和实现接口类型的一种映射是可复用的所以会将其以接口类型动态类型的组合为key进行缓存每次给非空接口赋值时都会先检查itab缓存类型断言类型断言根据断言的主题和断言成为的类型可以组合出四种空接口.(具体类型)比较空接口动态类型和具体类型的类型元数据非空接口.(具体类型)检查非空接口的itab是否是缓存中的特定itab空接口.(非空接口)先检查itab缓存若没有再检查类型的方法列表非空接口.(非空接口)先检查itab缓存若没有再检查类型的方法列表类型断言失败时也会把失败记录缓存到itab缓存参考https://www.bilibili.com/video/BV1iZ4y1T7zF/?spm_id_from333.999.0.08. 闭包Go语言的函数Go的函数在语法中可以作为对象可以作为变量的值、函数返回值或函数参数这样使用时会被分配function value对象是一个指针不指向代码段中的函数指令而是指向一个runtime.funcval结构体这个结构体中存储都代码段中的函数指令地址闭包闭包的作用在函数内部能使用已经脱离其生命周期的变量闭包的两个要素在函数中使用了外部定义的变量脱离了产生闭包的上下文函数依然能使用这些外部变量闭包的实现闭包相当于一个结构体存储了一个函数和一个关联的环境这个环境内部是若干符号和值的对应关系例如调用一个使用了在函数外部定义的变量的函数会在堆上生成一个闭包结构体保存着函数指针和捕获列表这个用到的外部变量就在捕获列表中被闭包捕获的变量如果从初始化赋值之后就没有被修改过就直接拷贝值到捕获列表中如果是被修改过的值变量这个值变量会改为堆分配在栈上储存地址捕获列表中也储存地址就实现了闭包函数和外层函数使用同一个变量。显然这种情况也会涉及变量逃逸如果捕获的是函数参数或返回值情况也不同但目标都是为了保证闭包捕获对象和外层对象的一致性二、Go的三种引用类型1. SliceSlice的底层数据结构Slice的结构体中存有两个int变量cap储存切片的最大容量len储存目前已经存储的元素数量另外还有一个指针变量指向底层数组这种实现方式导致了将一个切片赋值给另一个切片时二者会指向同一个底层数组Slice的扩容策略在切片容量小于1024时每次会扩容到之前的2倍当切片容量大于1024时每次会扩容到之前的1.25倍空切片和nil切片的区别空切片的指向底层数组的指针变量已经被初始化nil切片的指针变量是一个nil指针多个切片何时会指向同一个数组在 Go 语言中多个切片可能会指向同一个底层数组这种情况通常发生在以下几种情形切片的构造当你通过切片字面量创建一个切片时它会指向一个匿名的底层数组。如果通过切片字面量创建多个切片且它们有相同的起止索引它们将指向同一个底层数组。切片的切片如果你对一个切片进行切片操作新得到的切片将指向原始切片的底层数组。切片赋值如果你将一个切片赋值给另一个切片它们将指向同一个底层数组。使用切片函数或方法当你使用如 append, copy 函数或 cap 和 len 方法时如果新切片没有超出原始切片的容量它们将指向同一个底层数组。请注意当对切片进行扩容操作时如果新容量超出了原切片的容量Go 运行时会创建一个新的底层数组并将数据复制过去这时两个切片将指向不同的底层数组。2. MapMap的底层数据结构Map本身是一个指针指向底层哈希表Map的哈希表数据结构有下列成员当前键值对数量B:哈希函数的参数等于log2桶数所以该值每增大1桶数都会翻倍这个参数也能表征当前Map所能装载的最多键值对数目等于负载因子 * 2的B次方指向当前桶的指针溢出桶的数量溢出桶信息桶的数据结构一个桶里可以存放8个键值对为了使内存排布紧凑会将8个key存放在一起再将8个value存放在一起而且结构体最前面还会存放8个tophash值为对应哈希值的高8位有助于快速检索key桶中还存放着一个指向溢出桶的指针溢出桶和桶的数据结构一样为了避免频繁扩容在桶存满时会向连接的溢出桶中存放当哈希表要分配的桶数目大于2^4时就认为使用到溢出桶的几率较大就会预分配2 ^(B-4)个溢出桶溢出桶与常规桶在内存中连续Map是否有序无序因为扩容时Map的键值顺序会被打乱所以Go在遍历Map时会随机打乱顺序Map的哈希函数负载因子负载因子是Map的重要参数等于目前的键值对数除以桶数表征目前Map装载的键值对数量是否过多。当负载因子大于6.5时就会发生扩容Map的扩容策略、渐进式扩容Go Map的扩容行为受到负载因子和目前使用溢出桶数量这两个因素的制约负载因子大于6.5时发生翻倍扩容B桶数目翻倍每个旧桶的键值对都分流到两个新桶中负载因子不大于6.5但使用的溢出桶较多时B15溢出桶数目2 ^ B 或 B15溢出桶数目2 ^ 15发生等量扩容创建和旧桶数目一样多的新桶将所有键值对重哈希到新桶这样相当于释放了旧桶中之前删除掉的键值对空间使目前有效的键值对排列更为紧凑在扩容时会一次性开辟出新桶的内存使用两个字段记录旧桶的位置指针和迁移进度每次对Map进行读写操作时会完成一部分键值对迁移什么类型不能作为Map的Key在 Go 语言中map 的键类型必须是可以通过 操作符进行比较的类型。换句话说键类型必须是可比较的。可比较性是实现哈希表的基础因为哈希表依赖于键的快速比较来查找、插入和删除键值对。以下是 Go 语言中可以作为 map 键的数据类型内置类型如 int, float64, string, bool 等。结构体类型只要它们类型的所有字段类型都是可比较的。数组类型只要它们元素的类型是可比较的。接口类型只要它们值的动态类型是可比较的。以下类型不能用作 map 的键函数类型函数在 Go 中是不可比较的。字典类型map 类型本身是不可比较的。切片类型切片类型本身是不可比较的。通道类型通道类型本身是不可比较的。复合类型如果其包含不可比较的类型字段。结构体类型如果其包含不可比较的字段。接口类型如果其动态类型是不可比较的。请注意如果尝试使用不可比较的类型作为 map 的键Go 编译器将会报错。参考https://www.bilibili.com/video/BV1Sp4y1U7dJ/?spm_id_from333.999.0.03. ChannelChannel的底层数据结构因为channel需要支持协程并发访问其结构体中有一把锁有缓冲channel的缓冲区是一个数组和两个指针对应读写进度缓冲区可以看作一个环形因为读写指针在到达尾部后都会回到头部一个变量记录关闭状态最重要的channel有两个等待队列分别针对读和写当缓冲区已满时写入的元素会进入发送等待队列写入队列里存储的结构体会记录是哪个协程在等待读取同理参考https://www.bilibili.com/video/BV1hv411x7we?p29vd_sourced0c878ce37e7bc95bc121f75c62a5fc3Channel的读写特性空读写阻塞写关闭异常读关闭空零空读写阻塞向一个nil channel写入数据或试图从中读数据都会造成永久阻塞写关闭异常向一个已经关闭的channel中写入数据会panic读关闭空零从一个已经关闭的channel中读取数据会返回对应类型的零值参考https://static.kancloud.cn/aceld/golang/1958317三、 Go的内置包1. Contextcontext包的构成context接口四种实现emptyCtx实现为一个int变量cancelCtx用于获取取消通知的context会储存以当前节点为根节点的所有子context再根节点取消时会同步取消所有子节点timerCtx包装了cancelCtx可以手动取消也可以在规定时间到达之后自动触发取消valueCtx利用context存储键值对相同的key子节点的值会覆盖父节点的值六个方法Background()返回一个emptyCtxTODO()WithCancel()把一个context包装为cancelCtx并且提供一个取消函数调用取消函数即可取消该contextWithDeadline()指定一个时间点创建一个timerCtxWIthTimeout()指定一个时间段创建一个timerCtxWithValue()创建valueCtxcontext的作用在协程之间context的创建都是基于父context所以会形成一颗context树context的应用http包、gorm都实现了context便于http请求的超时自动取消等操作2. Sync atomicsync.Mutex 互斥锁底层数据结构的成员是一个int32的state字段和一个uint32的sema字段state代表锁的状态加锁和解锁都是通过atomic包提供的原子操作修改state字段。sema为信号量。state的每一位的代表不同的含义最低位代表加锁和解锁正常模式与饥饿模式在state1的正常状态下一个尝试加锁的goroutine会先自旋几次尝试通过原子操作获得锁若几次操作后仍不能获得锁会通过信号量排队等待。释放锁后信号量上等待的goroutine会和尚未进入信号量等待队列的自旋goroutine竞争往往自旋goroutine更容易获得锁所以如果等待的goroutine没有拿到锁就会被重新插入队列头部而非尾部。当一个goroutine等待锁的时间超过1ms之后会将当前的锁状态置为即饥饿模式在此模式下锁的所有权会从释放锁的goroutine直接交给等待队列头部的goroutine后来的goroutine会直接排队而非自旋。如果有一个等待时间小于1ms的goroutine或者等待队列最后一个goroutine获得了锁就会把锁从饥饿模式转换回正常模式正常模式下自旋和等待同时存在吞吐量更大性能更好饥饿模式下只有等待不会出现尾端延迟sync.RWMutexsync.WaitGroupsync.Poolsync.Oncesync.Condsycc.Mapsync.Map 是 Go 语言中并发安全的 Map 类型它提供了一些简单的并发访问控制允许在多线程环境下安全地访问和修改 Map 中的元素。sync.Map 采用了几个机制来提供线程安全性无锁读取在读取操作时sync.Map 通常会避免使用锁这允许在读取时的高并发性能。读写锁在写入操作时sync.Map 使用读写锁RWMutex来同步对 Map 的访问。这种锁允许多个 goroutine 同时读取但在写入时只允许一个 goroutine 访问。分离存储sync.Map 使用了两个存储结构一个用于读另一个用于写。写操作总是更新写存储而读操作则优先从读存储中读取。这种分离可以减少锁的竞争提高性能。延迟删除当键被标记为已删除时它不会被立即从存储中移除而是在后续的写操作中真正移除。这样可以减少不必要的锁操作。批量更新当写入操作累积到一定程度时sync.Map 会将所有写操作批量更新到读存储中以减少锁的持有时间。尽管如此sync.Map 在设计时还是牺牲了一些功能以优化性能。例如它没有提供获取 Map 长度的操作也没有提供删除键对应值的操作而是提供了键的删除操作。另外sync.Map 不提供迭代 Map 的方法这意味着你无法直接遍历其内容。因此在选择使用 sync.Map 还是传统的 map 时需要根据应用的具体需求来决定。如果你需要在多线程环境下进行大量的读操作sync.Map 可能是更好的选择。但如果你需要更复杂的操作或者不需要在多线程环境下使用 Map那么传统的 map 可能会更适合。sync包的很多实现都依赖atomic包参考https://chai2010.cn/advanced-go-programming-book/ch1-basic/ch1-05-mem.html3. Sort4. Reflect介绍反射反射的本质是把类型元数据暴露给用户使用。类型元数据、方法元数据这些信息本来都是未导出的私有的所以Reflect包中又保存了一套可导出的公有的参考https://www.bilibili.com/video/BV1WZ4y1M7r1/?spm_id_from333.999.0.0四、内存相关1. 虚拟内存介绍虚拟内存OS负责将虚拟内存映射到物理内存。映射的方法是分页32位机上一页为4KB。OS会以链表记录进程的控制信息即PCB进程控制块PCB中会有一个指针指向当前进程页目录的物理地址页目录也是一个内存页存储着一系列指针指向页表页表中记录物理内存页的地址。32位机的页目录记录1024个页表地址一个页表中记录1024个页地址一个内存页为4KB正好符合32位机最大寻址1024 * 1024 * 4 4GB内存在32位地址中前10位代表页目录中间10位代表内存页最后12位用于存储内存偏移量。因为每个进程都有自己的页目录所以可以实现进程之间的内存的地址隔离也可以通过映射到同一块物理内存实现进程间的内存共享MMUCPU的内存管理单元负责将程序的线性地址转换为物理地址TLBCPU会把已经转换过的地址映射关系缓存起来。切换进程时TLB缓存就会失效这也是切换进程代价较高的一个原因2. 内存管理策略堆内存Go程序启动时会申请一片内存分为512MB的spans、16GB的bitmap和512GB的arenaarena分为很多个page每个page大小为8KBSpan中存放指向mspan结构体的指针mspan的大小是page的整数倍每个mspan都被划分为若干固定大小的slotslot的大小有8B 到 80 KB 共67 种不同的规格其实还有大小为0作为第0种用于分配大对象Mspan之间有用一条双向链表链接起来在虚拟内存中mspan内部的页是连续的相同等级的mspan从属于同一个mcentral由同一把互斥锁管理mspan内部还会维护自己的bitmap每个P系统线程会维护自己的mcache将每个等级的mspan缓存一个其实是两个一个用于存储指针变量一个用于存储非指针变量方便GC线程取自己mcache里的mspan时是无锁的如果自己的mcache里对应等级的mspan用完了会去mcentral里申请mcentral中管理的mspan用完后会向mheap申请mheap是Go程序对于小对象即小于最小的mspan 8B的对象mspan会使用微对象分配器每16个B一块每个对象向上补齐到2的整数次幂分配对于大对象即大于最大的mspan 32KB的对象会直接向mheap申请足够数量的page组装成mspan随着内存申请的上升锁粒度也是上升的mcache无锁mcentral为mspan等级粒度锁mheap为全局锁栈内存栈的内存分配也使用mspan在调度器初始化时会初始化两个用于栈分配的全局对象stackPool面向32KB以下的栈分配栈大小必须是2的幂最小为2KB。stackLarge负责大于等于32KB的栈同堆内存分配一样每个P也有用于栈的本地缓存分配内存时先从本地缓存开始若没有再查找stackpool若没有再从堆内存分配一个32KB的内存块划分成对应大小的块后放入stackpool如果是大于等于32KB的对象就计算需要的页数每页8KB然后求以2为底求页的对数从stackLarge数组的对数下标位置寻找mspan链表进行分配栈的增长每次增长到之前的两倍栈的收缩GC时会收缩最小收缩到2KB协程运行结束时如果栈没有增长过仍未2KB会被加入有栈空闲协程队列。如果增长过就释放栈把协程放入无栈队列栈释放时也会分小于32KB和大于等于32KB两种情况。小于32KB的栈释放时先放回P的本地缓存如果本地缓存对应类型链表的内存总量大于32KB了就放入stackPool全局栈缓存中如果某个mspan的所有内存都释放了就会将mspan归还堆内存大于32KB的栈在GC时会被直接释放还给堆内存否则会先放回stackLarge3. 内存对齐为什么需要内存对齐CPU读取内存的方式通过地址总线32位机的总线宽度即机器字长为32最大可以寻址4G的内存同理64位机的总线宽度为64虽然虚拟内存是连续的但其实硬件的物理内存的布局是8个bank构成一个chip并不连续。所以读取一个chip的8个字节只需要一次读取操作但如果这8个字节横跨了两个chip就需要两次读取操作。为了减少读取次数需要内存对齐介绍内存对齐每种类型都会有一个对齐边界内存对齐规定每个对象的地址都需要是其类型的对齐边界的整数倍32位机的字长、指针长度和寄存器宽度都是4字节64位机都是8字节字长就是最大的对齐边界数据类型的对齐边界是类型长度和机器字长的较小值另外在不同机器上Go类型的长度也可能不同比如32位机的string长度为8字节64位机为16字节结构体的内存对齐结构体的内存对齐边界是所有成员的对齐边界的最大值其中的每个成员也要按各自的对齐边界进行对齐这就导致了结构体成员顺序不同可能造成其大小不同在对齐的存放所有成员后还需要将结构体长度扩充到其类型对齐边界的倍数。这种设计的初衷是保证结构体数组中每个元素的内存依然是对齐的参考https://www.bilibili.com/video/BV1Ja4y1i7AF/?spm_id_from333.999.0.04. 内存逃逸定义分配在栈内存上的变量由于会随着其生命周期结束自动销毁如果在生命周期结束后还需要访问这个变量就需要将这个对象改为在堆内存上分配使用堆内存的成本要高于栈所以编译器还是会尽量将变量在栈上分配只有在编译器不确定生命周期的变量需要在堆上分配决定变量是否要在堆上分配的是逃逸分析逃逸分析中几个常见的导致内存逃逸的场景函数返回了局部变量的指针函数的参数类型为接口分配占用内存较大的变量闭包5. GC三色标记法对象分为三种颜色标记黑、灰、白黑对象代表对象自身存活且其指向对象都已标记完成灰对象代表对象自身存活但其指向对象还未标记完成白对象代表对象尚未被标记到可能是垃圾对象标记开始前将根对象全局对象、栈上局部变量等置黑将其所指向的对象置灰标记规则是从灰对象出发将其所指向的对象都置灰. 所有指向对象都置灰后当前灰对象置黑标记结束后即不再有灰色对象白色对象就是不可达的垃圾对象需要进行清扫混合写屏障写屏障为什么会发生漏标GC协程已经标记为黑色的对象又建立了对白色对象的引用在这之后灰色对象删除了对白色对象的引用导致扫描灰色对象的时候检测不到对白色对象的引用了白色对象就被误删了有两种方案可以解决漏标问题强三色不变式白色对象不能被黑色对象直接引用弱三色不变式白色对象可以被黑色对象引用但要从某个灰对象出发仍然可达该白对象实现方法写屏障屏障机制类似于一个回调保护机制指的是在完成某个特定动作前会先完成屏障成设置的内容强三色不变式的实现插入写屏障一个黑色对象指向一个白色对象前会先触发屏障将白色对象置为灰色再建立引用弱三色不变式的实现删除写屏障一个白色对象即将被上游删除引用前会触发屏障将其置灰之后再删除上游指向其的引用因为屏障不能作用于栈对象所以Go1.8之后的实现采用的都是混合写屏障GC 开始前以栈为单位分批扫描将栈中所有对象置黑GC 期间栈上新创建对象直接置黑堆对象正常启用插入写屏障堆对象正常启用删除写屏障触发GC的时机内存使用超过阈值时 默认配置会在堆内存达到上一次垃圾收集的2倍时触发新一轮的垃圾收集没有触发GC的时间达到一定长度时默认为2分钟手动调用runtime.GC()时五、goroutine1. 进程、线程与协程的区别内核空间虚拟地址空间被划分为用户空间和内核空间操作系统运行在内核空间用户进程运行在用户空间。内核空间由所有进程的地址空间共享但不允许用户进程直接访问。进程的信息如PCB进程控制块就是保存于内核空间线程线程是进程的执行体线程执行时需要从进程的虚拟内存中分配栈空间存储数据称为线程栈在内核空间和用户空间各会分配一个段 成为用户栈和内核栈。线程切换到内核空间时就使用内核栈。WIndows线程的控制信息TCB也存储在PCB中Linux的线程信息和进程信息共用的是同一类结构体其中的某些字段相同实现多线程复用一些资源。同一个进程的线程共享地址空间、打开的文件句柄表。在CPU执行时寄存器中保存的都是线程信息所以线程是OS调度和执行的基本单位。一个进程中至少有一个线程即主线程由操作系统创建再由主线程创建其他线程。线程中再调用函数时又会在线程的栈上再分配函数调用栈。线程进行系统调用时会进入内核态此时就可以访问内核空间进程 线程的切换线程的切换线程的CPU时间片用完时CPU的硬件时钟会触发一次硬件中断从已经就绪的线程中挑选一个来执行。如果是同一进程的线程只需要把上一个线程的执行现场保存以来修改指令指针、栈指针等几个寄存器修改为下个线程的就可以所以同一个进程的多个线程切换成本较小。进程的切换除了线程切换也需要的寄存器内容进程切换还需要页目录的切换地址空间发生变化TLB缓存失效协程线程自己再创建几个执行体给他们分配并保存内存和执行入口等信息这些执行体就是协程协程的切换完全不涉及内核态内核也感知不到协程的存在所以协程又被称为**”用户态线程“**。协程切换时也需要像线程一样保存或恢复执行现场但是是由用户态完成的。2. GMP模型Go可执行文件的执行入口并不是main.main()在执行main.main()之前会以runtime.main()作为执行入口创建主协程main goroutine由main goroutine执行main.main()。在启动时会初始化几个全局变量g0主协程对应的g其协程栈在主线程栈上分配m0主线程的mg0和m0均持有互相的指针m0最初运行的g就是g0。记录所有g和p的变量GGoroutine 协程MMachine 系统线程真正负责执行代码的线程PProcessor每个M对应一个P本地队列与全局队列每个P拥有一个本地G队列对应的M可以直接从本地G队列中取出要执行的G。此外还有一个全局G队列如果P的本地队列已经满了在此P新创建的G就会加入全局队列。M优先从P的本地队列中获取G本地队列中没有G时会从全局队列中获取如果全局队列中也没有可以从其他P的队列中偷取一部分G3. goroutine池的实现4. goroutine一个系统级线程的栈可能是2MB一个goroutine的栈初始是2KB且能在使用过程中动态的伸缩goroutine的调度、上下文切换发生在用户态相比线程切换发生在内核态开销要小很多。六、Go运行时1. Go运行时负责什么Go运行时Go Runtime是Go编程语言的核心组件之一负责管理程序的执行和运行时环境。Go运行时在Go程序启动时被激活并在整个程序运行期间持续工作确保程序能够高效地运行。Go运行时负责以下几个核心任务垃圾回收Garbage CollectionGo运行时负责自动管理内存通过垃圾回收机制回收不再使用的内存减少内存泄漏的可能性。并发控制Go的运行时包含了其独特的调度器Scheduler它负责管理和调度goroutine确保它们能够并发地在多核处理器上执行。系统调用抽象Go运行时提供了对操作系统的抽象简化了文件操作、网络通信等系统调用使得开发者能够更容易地编写跨平台代码。环境变量和系统参数Go运行时允许程序读取和响应环境变量例如设置GC的参数、设置最大并发goroutine数量等。运行时类型信息Go运行时提供反射Reflection机制允许程序在运行时检查类型信息、修改值和调用方法。内存分配Go运行时负责管理内存的分配和释放包括堆内存和栈内存的分配。异常处理Go运行时提供了异常处理机制比如defer、panic和recover等使得开发者能够更容易地处理错误和异常。Go运行时是由Go语言运行时系统runtime system提供的它是Go标准库的一部分与Go的标准库一起被静态链接到最终的二进制文件中。这意味着Go运行时是随着Go程序一起发布的不需要依赖额外的运行时环境。这使得Go程序具有良好的可移植性能够在不同的操作系统和平台上运行而无需针对特定平台做特别配置。2. Go运行时是如何被执行的Go运行时Go Runtime不是指一个线程而是指Go语言运行时系统的一系列服务和功能。这些服务包括垃圾回收Garbage Collection、并发控制通过goroutine和channel、内存分配、系统调用抽象、环境变量和系统参数的读取等。当Go程序启动时它会创建一些用于执行用户代码的线程这些线程被称作工作线程M即Machine。每个工作线程都关联着一个操作系统线程即内核线程它负责执行Go代码。这些工作线程是由Go的调度器Scheduler进行管理的调度器确保goroutine在这些工作线程间公平且有效地进行调度执行。因此运行时本身并不占用一个线程去执行它是作为代码的一部分被加载到每个工作线程的上下文中并由工作线程执行。运行时提供的各种服务和功能被这些工作线程调用以执行如垃圾回收、并发控制等操作。总的来说Go运行时不是指一个线程而是指那些负责管理Go程序执行的服务和机制。这些机制和Go代码一样都是由工作线程M执行的。七、性能分析分析 Go 程序的性能通常涉及以下几个步骤性能基准测试使用 Go 的内置testing包和go test命令进行基准测试可以测量函数的执行时间。基准测试可以帮助你识别程序中的性能瓶颈。CPU Profiling使用pprof工具进行 CPU 分析可以了解程序在运行时哪些函数最消耗 CPU 资源。这通常通过net/http/pprof包或者runtime/pprof包来实现。内存分析使用pprof进行内存分析可以查看程序中哪些部分占用了最多的内存有助于发现内存泄漏等问题。阻塞和非阻塞分析使用pprof可以分析程序中的阻塞调用识别那些可能导致程序性能下降的同步调用。跟踪分析使用pprof的追踪功能可以记录程序执行过程中的一系列事件以了解程序的行为和性能。可视化使用go tool pprof提供的可视化工具如web命令可以直观地查看性能数据。火焰图火焰图是一种可视化性能分析数据的工具可以直观地显示程序的调用栈帮助快速定位性能瓶颈。代码审查审查代码查看是否有可以优化的算法减少不必要的操作或者重构以提高性能。下面是使用pprof进行性能分析的基本步骤导入必要的包import(net/http_net/http/pprof)开启 HTTP 服务器以提供性能分析数据funcmain(){// ...// 开启 pprof 分析gohttp.ListenAndServe(:8080,nil)// ...}使用go tool pprof分析数据go tool pprof http://localhost:8080/debug/pprof/profile执行性能分析操作例如执行top或list命令查看分析结果。使用web命令生成可视化的火焰图等。根据分析的结果进行优化。请注意优化程序性能时要确保所做的更改不仅提高了程序的性能同时没有pprof可以查看的资源内存每个函数使用的内存 火焰图也可以使用memStat打印更精准cpu每个函数使用的CPU 火焰图goroutine当前所有启动的goroutine和他们正在执行的函数mutexthreadcreate