news 2026/7/11 13:45:20

《 九 阴 真 经 卷 一 》Golang

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
《 九 阴 真 经 卷 一 》Golang

《 九 阴 真 经 卷 一 》Golang

文章目录

  • 《 九 阴 真 经 卷 一 》Golang
    • 一. Go语言的原语、关键字、语言特性
      • 1. make、new
      • 2. range
      • 3. defer、recover
      • 4. rune类型
      • 5. select
      • 6. init函数
      • 7. interface与类型断言
      • 8. 闭包
    • 二、Go的三种引用类型
      • 1. Slice
        • 多个切片何时会指向同一个数组
      • 2. Map
        • 什么类型不能作为Map的Key
      • 3. Channel
    • 三、 Go的内置包
      • 1. Context
      • 2. Sync & atomic
      • 3. Sort
      • 4. Reflect
    • 四、内存相关
      • 1. 虚拟内存
      • 2. 内存管理策略
      • 3. 内存对齐
      • 4. 内存逃逸
      • 5. GC
    • 五、goroutine
      • 1. 进程、线程与协程的区别
      • 2. GMP模型
      • 3. goroutine池的实现
      • 4. goroutine
    • 六、Go运行时
      • 1. Go运行时负责什么:
      • 2. Go运行时是如何被执行的
  • 七、性能分析
      • pprof可以查看的资源:

一. Go语言的原语、关键字、语言特性

1. make、new

  • make与new的区别
    • 相同点:
      • make和new都是在堆上分配内存
    • 不同点:
      • make仅能用于三种引用类型
      • new会将开辟的内存区域清零,make不是简单的清零,而是初始化底层数据结构
      • make返回的是引用类型对象本身,new返回的是指针

2. range

  • range时局部变量的生存周期
    • 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_from=333.999.0.0

4. rune类型

  • Go的默认编码类型是utf-8
  • 介绍一下rune类型
    • rune等同于int32类型,用于处理unicode或utf-8字符
    • byte等同于int8类型,用于处理ascii字符
  • 单引号’’ 双引号"" 反引号``的区别
    • 单引号表示byte类型或rune类型,即一个字符
    • 双引号表示字符串,即字符数组
    • 反引号表示字符串,且不支持转义

5. select

  • select的作用

    • 用多个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?p=29&vd_source=d0c878ce37e7bc95bc121f75c62a5fc3

6. init函数

  • 不同包的init函数执行顺序
    • 从被导入的最深层的包开始执行,直至main包
    • 每个包的init函数只会执行一次
  • 同一个包的多个文件的init函数执行顺序
    • 按文件名顺序执行
  • init函数和包级别变量初始化的先后
    • 包级别变量的初始化先于init函数

7. interface与类型断言

  • 空接口和非空接口各自的实现
    • 空接口的实现
      • 空接口的结构体为eface,主要字段为接口的动态类型,是一个类型元数据的指针,和一个unsafe.Pointer,指向接口的动态值。赋值之前,这两个字段都为nil
    • 非空接口的实现
      • 非空接口的结构体为iface,主要字段为一个接口动态值指针和一个指向itab,itab结构体中会存放接口需要的方法列表、动态类型元数据和从类型元数据中拷贝而来的,接口所需方法实现的地址(这样在使用接口调用方法的时候就不需要查询类型元数据)。特殊的,itab结构体代表接口和实现接口类型的一种映射,是可复用的,所以会将其以接口类型+动态类型的组合为key进行缓存,每次给非空接口赋值时都会先检查itab缓存
  • 类型断言
    • 类型断言根据断言的主题和断言成为的类型可以组合出四种:
      • 空接口.(具体类型):比较空接口动态类型和具体类型的类型元数据
      • 非空接口.(具体类型):检查非空接口的itab是否是缓存中的特定itab
      • 空接口.(非空接口):先检查itab缓存,若没有再检查类型的方法列表
      • 非空接口.(非空接口):先检查itab缓存,若没有再检查类型的方法列表
    • 类型断言失败时,也会把失败记录缓存到itab缓存
  • 参考https://www.bilibili.com/video/BV1iZ4y1T7zF/?spm_id_from=333.999.0.0

8. 闭包

  • Go语言的函数
    • Go的函数在语法中可以作为对象,可以作为变量的值、函数返回值或函数参数,这样使用时会被分配function value对象,是一个指针,不指向代码段中的函数指令,而是指向一个runtime.funcval结构体,这个结构体中存储都代码段中的函数指令地址
  • 闭包
    • 闭包的作用:在函数内部能使用已经脱离其生命周期的变量
    • 闭包的两个要素:
      • 在函数中使用了外部定义的变量
      • 脱离了产生闭包的上下文,函数依然能使用这些外部变量
    • 闭包的实现:
      • 闭包相当于一个结构体,存储了一个函数和一个关联的环境,这个环境内部是若干符号和值的对应关系
      • 例如,调用一个使用了在函数外部定义的变量的函数,会在堆上生成一个闭包结构体,保存着函数指针和捕获列表,这个用到的外部变量就在捕获列表中
      • 被闭包捕获的变量,如果从初始化赋值之后就没有被修改过,就直接拷贝值到捕获列表中;如果是被修改过的值变量,这个值变量会改为堆分配,在栈上储存地址,捕获列表中也储存地址,就实现了闭包函数和外层函数使用同一个变量。显然,这种情况也会涉及变量逃逸;如果捕获的是函数参数或返回值,情况也不同,但目标都是为了保证闭包捕获对象和外层对象的一致性

二、Go的三种引用类型

1. Slice

  • Slice的底层数据结构
    • Slice的结构体中存有两个int变量,cap储存切片的最大容量,len储存目前已经存储的元素数量,另外还有一个指针变量,指向底层数组
    • 这种实现方式导致了将一个切片赋值给另一个切片时,二者会指向同一个底层数组
  • Slice的扩容策略
    • 在切片容量小于1024时,每次会扩容到之前的2倍;当切片容量大于1024时,每次会扩容到之前的1.25倍
  • 空切片和nil切片的区别
    • 空切片的指向底层数组的指针变量已经被初始化,nil切片的指针变量是一个nil指针
多个切片何时会指向同一个数组

在 Go 语言中,多个切片可能会指向同一个底层数组,这种情况通常发生在以下几种情形:

  • 切片的构造:当你通过切片字面量创建一个切片时,它会指向一个匿名的底层数组。如果通过切片字面量创建多个切片,且它们有相同的起止索引,它们将指向同一个底层数组。
  • 切片的切片:如果你对一个切片进行切片操作,新得到的切片将指向原始切片的底层数组。
  • 切片赋值:如果你将一个切片赋值给另一个切片,它们将指向同一个底层数组。
  • 使用切片函数或方法:当你使用如 append, copy 函数或 cap 和 len 方法时,如果新切片没有超出原始切片的容量,它们将指向同一个底层数组。
  • 请注意,当对切片进行扩容操作时,如果新容量超出了原切片的容量,Go 运行时会创建一个新的底层数组并将数据复制过去,这时两个切片将指向不同的底层数组。

2. Map

  • Map的底层数据结构
    • 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,但使用的溢出桶较多时(B<15&&溢出桶数目>=2 ^ B 或 B>=15&&溢出桶数目>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_from=333.999.0.0

3. Channel

  • Channel的底层数据结构
    • 因为channel需要支持协程并发访问,其结构体中有一把锁
    • 有缓冲channel的缓冲区是一个数组,和两个指针,对应读写进度,缓冲区可以看作一个环形,因为读写指针在到达尾部后都会回到头部
    • 一个变量记录关闭状态
    • 最重要的,channel有两个等待队列,分别针对读和写
      • 当缓冲区已满时,写入的元素会进入发送等待队列,写入队列里存储的结构体会记录是哪个协程在等待
      • 读取同理
    • 参考https://www.bilibili.com/video/BV1hv411x7we?p=29&vd_source=d0c878ce37e7bc95bc121f75c62a5fc3
  • Channel的读写特性
    • 空读写阻塞,写关闭异常,读关闭空零
      • 空读写阻塞:向一个nil channel写入数据或试图从中读数据都会造成永久阻塞
      • 写关闭异常:向一个已经关闭的channel中写入数据,会panic
      • 读关闭空零:从一个已经关闭的channel中读取数据,会返回对应类型的零值
    • 参考https://static.kancloud.cn/aceld/golang/1958317

三、 Go的内置包

1. Context

  • context包的构成:
    • context接口
    • 四种实现:
      • emptyCtx:实现为一个int变量
      • cancelCtx:用于获取取消通知的context,会储存以当前节点为根节点的所有子context,再根节点取消时,会同步取消所有子节点
      • timerCtx:包装了cancelCtx,可以手动取消,也可以在规定时间到达之后自动触发取消
      • valueCtx:利用context存储键值对,相同的key,子节点的值会覆盖父节点的值
    • 六个方法:
      • Background():返回一个emptyCtx
      • TODO()
      • WithCancel():把一个context包装为cancelCtx,并且提供一个取消函数,调用取消函数即可取消该context
      • WithDeadline():指定一个时间点创建一个timerCtx
      • WIthTimeout():指定一个时间段创建一个timerCtx
      • WithValue():创建valueCtx
  • context的作用
    • 在协程之间
    • context的创建都是基于父context,所以会形成一颗context树
  • context的应用
    • http包、gorm都实现了context,便于http请求的超时自动取消等操作

2. Sync & atomic

  • sync.Mutex 互斥锁

    • 底层数据结构的成员是一个int32的state字段和一个uint32的sema字段,state代表锁的状态,加锁和解锁都是通过atomic包提供的原子操作修改state字段。sema为信号量。state的每一位的代表不同的含义,最低位代表加锁和解锁
    • 正常模式与饥饿模式:在state=1的正常状态下,一个尝试加锁的goroutine会先自旋几次,尝试通过原子操作获得锁,若几次操作后仍不能获得锁,会通过信号量排队等待。释放锁后,信号量上等待的goroutine会和尚未进入信号量等待队列的自旋goroutine竞争,往往自旋goroutine更容易获得锁,所以如果等待的goroutine没有拿到锁,就会被重新插入队列头部而非尾部。当一个goroutine等待锁的时间超过1ms之后,会将当前的锁状态置为,即饥饿模式,在此模式下,锁的所有权会从释放锁的goroutine直接交给等待队列头部的goroutine,后来的goroutine会直接排队,而非自旋。如果有一个等待时间小于1ms的goroutine,或者等待队列最后一个goroutine获得了锁,就会把锁从饥饿模式转换回正常模式
    • 正常模式下自旋和等待同时存在,吞吐量更大,性能更好
    • 饥饿模式下只有等待,不会出现尾端延迟
  • sync.RWMutex

  • sync.WaitGroup

  • sync.Pool

  • sync.Once

  • sync.Cond

  • sycc.Map
    sync.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.html

3. Sort

4. Reflect

  • 介绍反射
    • 反射的本质是把类型元数据暴露给用户使用。类型元数据、方法元数据这些信息本来都是未导出的(私有的),所以Reflect包中又保存了一套可导出的(公有的)
  • 参考https://www.bilibili.com/video/BV1WZ4y1M7r1/?spm_id_from=333.999.0.0

四、内存相关

1. 虚拟内存

  • 介绍虚拟内存
    • OS负责将虚拟内存映射到物理内存。映射的方法是分页,32位机上一页为4KB。OS会以链表记录进程的控制信息,即PCB进程控制块,PCB中会有一个指针指向当前进程页目录的物理地址,页目录也是一个内存页,存储着一系列指针,指向页表,页表中记录物理内存页的地址。32位机的页目录记录1024个页表地址,一个页表中记录1024个页地址,一个内存页为4KB,正好符合32位机最大寻址1024 * 1024 * 4 = 4GB内存,在32位地址中,前10位代表页目录,中间10位代表内存页,最后12位用于存储内存偏移量。
    • 因为每个进程都有自己的页目录,所以可以实现进程之间的内存的地址隔离,也可以通过映射到同一块物理内存实现进程间的内存共享
    • MMU:CPU的内存管理单元,负责将程序的线性地址转换为物理地址
    • TLB:CPU会把已经转换过的地址映射关系缓存起来。切换进程时,TLB缓存就会失效,这也是切换进程代价较高的一个原因

2. 内存管理策略

  • 堆内存
    • Go程序启动时,会申请一片内存,分为512MB的spans、16GB的bitmap和512GB的arena
    • arena分为很多个page,每个page大小为8KB
    • Span中存放指向mspan结构体的指针,mspan的大小是page的整数倍,每个mspan都被划分为若干固定大小的slot,slot的大小有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时会被直接释放还给堆内存,否则会先放回stackLarge

3. 内存对齐

  • 为什么需要内存对齐
    • 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_from=333.999.0.0

4. 内存逃逸

  • 定义:分配在栈内存上的变量由于会随着其生命周期结束自动销毁,如果在生命周期结束后还需要访问这个变量,就需要将这个对象改为在堆内存上分配
  • 使用堆内存的成本要高于栈,所以编译器还是会尽量将变量在栈上分配,只有在编译器不确定生命周期的变量需要在堆上分配,决定变量是否要在堆上分配的是逃逸分析
  • 逃逸分析中几个常见的导致内存逃逸的场景
    • 函数返回了局部变量的指针
    • 函数的参数类型为接口
    • 分配占用内存较大的变量
    • 闭包

5. GC

  • 三色标记法
    • 对象分为三种颜色标记:黑、灰、白
    • 黑对象代表,对象自身存活,且其指向对象都已标记完成
    • 灰对象代表,对象自身存活,但其指向对象还未标记完成
    • 白对象代表,对象尚未被标记到,可能是垃圾对象
    • 标记开始前,将根对象(全局对象、栈上局部变量等)置黑,将其所指向的对象置灰
    • 标记规则是,从灰对象出发,将其所指向的对象都置灰. 所有指向对象都置灰后,当前灰对象置黑
    • 标记结束后(即不再有灰色对象),白色对象就是不可达的垃圾对象,需要进行清扫
  • 混合写屏障
    • 写屏障
      • 为什么会发生漏标?GC协程已经标记为黑色的对象,又建立了对白色对象的引用,在这之后,灰色对象删除了对白色对象的引用,导致扫描灰色对象的时候,检测不到对白色对象的引用了,白色对象就被误删了
    • 有两种方案可以解决漏标问题:
      • 强三色不变式:白色对象不能被黑色对象直接引用
      • 弱三色不变式:白色对象可以被黑色对象引用,但要从某个灰对象出发仍然可达该白对象
    • 实现方法:写屏障,屏障机制类似于一个回调保护机制,指的是在完成某个特定动作前,会先完成屏障成设置的内容
      • 强三色不变式的实现:插入写屏障:一个黑色对象指向一个白色对象前,会先触发屏障将白色对象置为灰色,再建立引用
      • 弱三色不变式的实现:删除写屏障:一个白色对象即将被上游删除引用前,会触发屏障将其置灰,之后再删除上游指向其的引用
      • 因为屏障不能作用于栈对象,所以Go1.8之后的实现采用的都是混合写屏障:
        • GC 开始前,以栈为单位分批扫描,将栈中所有对象置黑
        • GC 期间,栈上新创建对象直接置黑
        • 堆对象正常启用插入写屏障
        • 堆对象正常启用删除写屏障
  • 触发GC的时机:
    • 内存使用超过阈值时 (默认配置会在堆内存达到上一次垃圾收集的2倍时,触发新一轮的垃圾收集)
    • 没有触发GC的时间达到一定长度时(默认为2分钟)
    • 手动调用runtime.GC()时

五、goroutine

1. 进程、线程与协程的区别

  • 内核空间
    • 虚拟地址空间被划分为用户空间和内核空间,操作系统运行在内核空间,用户进程运行在用户空间。内核空间由所有进程的地址空间共享,但不允许用户进程直接访问。进程的信息,如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:主线程的m,g0和m0均持有互相的指针,m0最初运行的g就是g0。
    • 记录所有g和p的变量
  • G:Goroutine 协程
  • M:Machine 系统线程,真正负责执行代码的线程
  • P:Processor,每个M对应一个P
  • 本地队列与全局队列:每个P拥有一个本地G队列,对应的M可以直接从本地G队列中取出要执行的G。此外还有一个全局G队列,如果P的本地队列已经满了,在此P新创建的G就会加入全局队列。M优先从P的本地队列中获取G,本地队列中没有G时,会从全局队列中获取,如果全局队列中也没有,可以从其他P的队列中偷取一部分G

3. goroutine池的实现

4. goroutine

  • 一个系统级线程的栈可能是2MB,一个goroutine的栈初始是2KB,且能在使用过程中动态的伸缩
  • goroutine的调度、上下文切换发生在用户态,相比线程切换发生在内核态,开销要小很多。

六、Go运行时

1. Go运行时负责什么:

Go运行时(Go Runtime)是Go编程语言的核心组件之一,负责管理程序的执行和运行时环境。Go运行时在Go程序启动时被激活,并在整个程序运行期间持续工作,确保程序能够高效地运行。

Go运行时负责以下几个核心任务:

垃圾回收(Garbage Collection):Go运行时负责自动管理内存,通过垃圾回收机制回收不再使用的内存,减少内存泄漏的可能性。

并发控制: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 程序的性能通常涉及以下几个步骤:

  1. 性能基准测试
    使用 Go 的内置testing包和go test命令进行基准测试,可以测量函数的执行时间。基准测试可以帮助你识别程序中的性能瓶颈。

  2. CPU Profiling
    使用pprof工具进行 CPU 分析,可以了解程序在运行时哪些函数最消耗 CPU 资源。这通常通过net/http/pprof包或者runtime/pprof包来实现。

  3. 内存分析
    使用pprof进行内存分析,可以查看程序中哪些部分占用了最多的内存,有助于发现内存泄漏等问题。

  4. 阻塞和非阻塞分析
    使用pprof可以分析程序中的阻塞调用,识别那些可能导致程序性能下降的同步调用。

  5. 跟踪分析
    使用pprof的追踪功能,可以记录程序执行过程中的一系列事件,以了解程序的行为和性能。

  6. 可视化
    使用go tool pprof提供的可视化工具,如web命令,可以直观地查看性能数据。

  7. 火焰图
    火焰图是一种可视化性能分析数据的工具,可以直观地显示程序的调用栈,帮助快速定位性能瓶颈。

  8. 代码审查
    审查代码,查看是否有可以优化的算法,减少不必要的操作,或者重构以提高性能。

下面是使用pprof进行性能分析的基本步骤:

  1. 导入必要的包:
import("net/http"_"net/http/pprof")
  1. 开启 HTTP 服务器以提供性能分析数据:
funcmain(){// ...// 开启 pprof 分析gohttp.ListenAndServe(":8080",nil)// ...}
  1. 使用go tool pprof分析数据:
go tool pprof http://localhost:8080/debug/pprof/profile
  1. 执行性能分析操作(例如,执行toplist命令查看分析结果)。

  2. 使用web命令生成可视化的火焰图等。

  3. 根据分析的结果进行优化。

请注意,优化程序性能时,要确保所做的更改不仅提高了程序的性能,同时没有

pprof可以查看的资源:

  • 内存:每个函数使用的内存 火焰图(也可以使用memStat打印,更精准)
  • cpu:每个函数使用的CPU 火焰图
  • goroutine:当前所有启动的goroutine和他们正在执行的函数
  • mutex
  • threadcreate
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/7 13:48:44

Compose 基础 - 其它设置、资源获取

二、对齐方式 Alignment 2.1 平面方向&#xff08;2D&#xff09; TopStartTopCenterTopEndCenterStartCenterCenterEndBottomStartBottomCenterBottomEnd 2.2 线性方向&#xff08;1D&#xff09; 2.2.1 设置 verticalAlignment TopCenterVerticallyBottom 2.2.2 设置 hor…

作者头像 李华
网站建设 2026/7/7 7:47:17

Win11下VMware配置Ubuntu虚拟机的完整指南

1. 环境准备&#xff1a;VMware与Ubuntu镜像下载 在Windows 11上玩转Ubuntu虚拟机&#xff0c;首先得准备好两样东西&#xff1a;VMware Workstation Pro和Ubuntu系统镜像。我推荐使用VMware 16 Pro版本&#xff0c;这个版本对Win11的兼容性最好&#xff0c;实测运行稳定不卡顿…

作者头像 李华
网站建设 2026/7/7 10:32:32

QuTiP量子计算工具包安装配置指南

QuTiP量子计算工具包安装配置指南 【免费下载链接】qutip QuTiP: Quantum Toolbox in Python 项目地址: https://gitcode.com/gh_mirrors/qu/qutip 需求定位&#xff1a;选择适合你的安装路径 在开始安装QuTiP前&#xff0c;需要根据用户类型和使用场景选择合适的安装方…

作者头像 李华
网站建设 2026/7/6 14:54:12

Coding Plan技术解析:AI编程时代的成本优化与效率革命

一、Coding Plan的本质&#xff1a;从Token经济到订阅制革命在AI编程领域&#xff0c;传统按Token计费模式正面临效率与成本的双重挑战。以OpenClaw为例&#xff0c;单个中等复杂度开发任务需20-50轮对话&#xff0c;消耗数万Token&#xff0c;单任务成本达数元至数十元。对于日…

作者头像 李华
网站建设 2026/7/3 2:51:35

选第一个Linux发行版前,必想的5个关键问题

作为一名专注开源生态与桌面创新的科技自媒体博主,我经常收到粉丝私信:“哥,我终于下定决心试试Linux了,但面对几百个发行版,我该从哪个开始?” 没错,Linux的自由在于“百花齐放”,但对新人来说,选择恐惧症才是最大的拦路虎。今天这篇文章,就帮你系统讲解在挑选第一…

作者头像 李华
网站建设 2026/7/7 18:27:20

QT 5.6.1虚拟键盘在arm-linux上的实现与优化

1. 为什么要在ARM-Linux上实现QT虚拟键盘&#xff1f; 在嵌入式设备开发中&#xff0c;用户输入一直是个头疼的问题。想象一下&#xff0c;你正在开发一款智能家居控制面板或者工业手持终端&#xff0c;设备没有物理键盘&#xff0c;但用户又需要输入文字——这时候虚拟键盘就成…

作者头像 李华