更新几篇golang文章
This commit is contained in:
BIN
public/api/i/2025/07/19/uje4vo-1.webp
Normal file
BIN
public/api/i/2025/07/19/uje4vo-1.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
1969
src/content/posts/Golang/Gin框架快速入门.md
Normal file
1969
src/content/posts/Golang/Gin框架快速入门.md
Normal file
File diff suppressed because it is too large
Load Diff
326
src/content/posts/Golang/Go_map底层结构.md
Normal file
326
src/content/posts/Golang/Go_map底层结构.md
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
---
|
||||||
|
title: Go_map底层结构
|
||||||
|
published: 2025-07-19
|
||||||
|
description: ''
|
||||||
|
image: 'https://blog.meowrain.cn/api/i/2025/07/19/uje4vo-1.webp'
|
||||||
|
tags: [切片, Golang, Go]
|
||||||
|
category: 'Go'
|
||||||
|
draft: false
|
||||||
|
lang: ''
|
||||||
|
---
|
||||||
|
# Golang map底层数据结构
|
||||||
|
|
||||||
|
<https://golang.design/go-questions/map/principal/>
|
||||||
|
|
||||||
|
[Golang map 实现原理](https://mp.weixin.qq.com/s?__biz=MzkxMjQzMjA0OQ==&mid=2247483868&idx=1&sn=6e954af8e5e98ec0a9d9fc5c8ceb9072&chksm=c10c4f02f67bc614ff40a152a848508aa1631008eb5a600006c7552915d187179c08d4adf8d7&scene=0&xtrack=1&subscene=90#rd)
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
map是一种常用的数据结构,核心特征包括下面三点:
|
||||||
|
|
||||||
|
- 存储基于key-value对映射的模式
|
||||||
|
- 基于key维度实现存储数据的去重
|
||||||
|
- 读,写,删操作控制,时间复杂度O(1)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 初始化方法
|
||||||
|
|
||||||
|
```go
|
||||||
|
map1 := make(map[string]int)
|
||||||
|
|
||||||
|
map2 := map[string]int{
|
||||||
|
"m1": 1,
|
||||||
|
"m2":2,
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### key 类型要求
|
||||||
|
|
||||||
|
map中,key的数据类型必须是可以比较的类型,slice,chan,func,map不可比较,所以不能作为map的key
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# 核心原理
|
||||||
|
|
||||||
|
map又称为hash map,算法上基于hash实现key的映射和寻址,在数据结构上基于桶数组实现key-value对的存储
|
||||||
|
|
||||||
|
以一组key-value对写入map的流程进行简述:
|
||||||
|
|
||||||
|
1. 通过哈希方法去的key的hash值‘
|
||||||
|
2. hash值对同数组长度取模,确定它所属的桶
|
||||||
|
3. 在桶中插入key value对
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## hash
|
||||||
|
|
||||||
|
hash 译作散列,是一种将任意长度的输入压缩到某一固定长度的输出摘要的过程,由于这种转换属于压缩映射,输入空间远大于输出空间,因此不同输入可能会映射成相同的输出结果. 此外,hash在压缩过程中会存在部分信息的遗失,因此这种映射关系具有不可逆的特质.
|
||||||
|
|
||||||
|
1. hash的可重入性: 相同的key,必然产生相同的hash值
|
||||||
|
2. hash的离散性: 只要两个key不相同,不论他们相似度的高低,产生的hash值会在整个输出域内均匀地离散化
|
||||||
|
3. hash的单向性: 企图通过hash值反向映射会key是无迹可寻的。
|
||||||
|
4. hash冲突: 由于输入域无穷大,输出域有限,必然存在不同key映射到相同hash值的情况,这种情况叫做哈希冲突
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 桶数组
|
||||||
|
|
||||||
|
map中,会通过长度为2的整数次幂的桶数组进行key-value对的存储
|
||||||
|
|
||||||
|
1. 每个桶固定可以存放8个key-value对
|
||||||
|
2. 倘若超过8个key-value对打到桶数组的同一个索引当中,此时会通过创建桶链表的方式来化解这个问题。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 拉链法解决hash冲突
|
||||||
|
|
||||||
|
首先,由于hash冲突的存在,不同的key可能存在相同的hash值
|
||||||
|
|
||||||
|
再者,hash值会对桶数组长度取模,因此不同的hash值可能被打到同一个桶中
|
||||||
|
|
||||||
|
综上,不同的key-value可能被映射到map的同一个桶当中。
|
||||||
|
|
||||||
|
拉链法中,将命中同一个桶的元素通过链表的形式进行连接,因此便于动态扩展
|
||||||
|
|
||||||
|
> 只有当一个桶已经满了(8 个 kv 对),并且又有新的 key 哈希到这个桶时,才会创建溢出桶,并将新的 key-value 对存储到溢出桶中,然后将该溢出桶链接到原桶的尾部。 后续再有冲突的 kv 对,也会被添加到溢出桶或者新的溢出桶中,形成一个链表。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 开放寻址法解决hash冲突
|
||||||
|
|
||||||
|
> 开放寻址法是一种解决哈希冲突的方法,它在哈希表中寻找另一个空闲位置存储冲突的元素,也就是说,所有元素都直接存储在哈希表的桶中
|
||||||
|
>
|
||||||
|
> 开放寻址法是一种在哈希表中解决冲突的方法。当两个不同的键映射到同一个索引位置时,就会发生冲突。开放寻址法不是使用链表等额外的数据结构来存储冲突的键值对,而是尝试在哈希表本身中寻找一个空闲的位置来存储新的键值对。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
常见开放寻址技术:
|
||||||
|
|
||||||
|
- 线性寻址: 如果在索引`i`发生冲突,线性探测会依次检查`i+1`,`i+2`,`i+3`等位置,直到找到一个空闲的槽位
|
||||||
|
- 二次探测检查 `i + 1^2`、`i + 2^2`、`i + 3^2` 等位置。与线性探测相比,这有助于减少聚集现象。
|
||||||
|
- 双重哈希: 双重哈希使用第二个哈希函数来确定探测的步长。如果第一个哈希函数在索引`i`导致哈希冲突,第二个哈希函数hash2(key)用于确定探测的间隔(例如,`i + hash2(key)`、`i + 2*hash2(key)`、`i + 3*hash2(key)` 等)。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
我们的golang map解决哈希冲突的方式结合了拉链法和开放寻址法。
|
||||||
|
|
||||||
|
- 桶: map的底层数据结构是一个桶数组,每个桶严格意义上是一个单向桶链表
|
||||||
|
- 桶的大小: 每个桶可以固定存放8个key value对
|
||||||
|
- 当key命中一个桶的时候,首先根据开放寻址法,在桶的8个位置中寻找空位进行插入
|
||||||
|
- 倘若8个位置都已经被占满,就基于桶的溢出桶指针,找到下一个桶(重复第三步)
|
||||||
|
- 倘若遍历到链表尾部,还没找到空位,就用拉链法,在桶链表尾部接入新桶,并且插入key-value对
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 扩容性能优化
|
||||||
|
|
||||||
|
倘若map的桶数组长度固定不变,那么随着key-value对数量的增长,当一个桶下挂载的key-value达到一定的量级,此时操作的时间复杂度会趋于线性,无法满足诉求。
|
||||||
|
|
||||||
|
**桶数组长度固定不变 + key-value 对数量持续增加 => 哈希冲突加剧 => Bucket 链表变长 => 查找/插入/删除 需要遍历长链表 => 操作时间复杂度接近 O(n) (线性)**
|
||||||
|
|
||||||
|
因此在设计上,map桶的数组长度会随着key-value对的数量变化而实时调整。保证每个桶内的key-value对数量始终控制在常量级别。
|
||||||
|
|
||||||
|
扩容类型分为:
|
||||||
|
|
||||||
|
- 增量扩容
|
||||||
|
- 等量扩容
|
||||||
|
|
||||||
|
### 增量扩容
|
||||||
|
|
||||||
|
触发条件: `key-value总数 / 桶数组长度 > 6.5`的时候,发生增量扩容
|
||||||
|
|
||||||
|
扩容方式: 桶数组长度增长为原来的`两倍`
|
||||||
|
|
||||||
|
目的: 减少负载因子,降低平均查找时间
|
||||||
|
|
||||||
|
负载因子: `key-value总数 / 桶的数量`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 等量扩容
|
||||||
|
|
||||||
|
触发条件: 当桶内溢出桶数量大于等于2^B时(B 为桶数组长度的指数,B 最大取 15),发生等量扩容。)
|
||||||
|
|
||||||
|
扩容方式: 桶的长度保持为原来的值
|
||||||
|
|
||||||
|
**目的:** 解决哈希冲突严重的问题,可能由于哈希函数选择不佳导致大量 key 映射到相同的桶,即使负载因子不高,也会出现大量溢出桶。 等量扩容旨在重新组织数据,减少溢出桶的数量。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 渐进式扩容
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# 数据结构
|
||||||
|
|
||||||
|
## hmap
|
||||||
|
|
||||||
|
```go
|
||||||
|
type hmap struct {
|
||||||
|
count int // map中键值对的数量
|
||||||
|
flags uint8 // map的状态标志位,用来指示map的当前状态(正在写入,正在扩容等)
|
||||||
|
B uint8 // buckets 数组的对数大小,2^B 是buckets数组的长度,比如B是5,那么桶数组的长度就是2^5 = 32
|
||||||
|
noverflow uint16 //溢出桶数量的近似值 用来判断是否需要扩容
|
||||||
|
hash0 uint32 // 哈希种子
|
||||||
|
buckets unsafe.Pointer //指向bucket数组的指针,数组大小为2 ^ B,如果count == 0,那么buckets可能为nil
|
||||||
|
oldbuckets unsafe.Pointer // 如果发生扩容,指向旧的buckets数组
|
||||||
|
nevacuate uintptr // 扩容的时候,表示旧buckcet数组已经迁移到新bucket数组的数量计数器
|
||||||
|
extra *mapextra // 可选字段,用来保存overflow buckets的信息
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
flags: map状态标识,其包含的主要状态为(这里面牵扯到很多概念还没有涉及,可以先大致的了解一下各自的含义)
|
||||||
|
|
||||||
|
- iterator(`0b0001`): 当前map可能正在被遍历
|
||||||
|
- oldIterator(`0b0010`): 当前map的旧桶可能正在被遍历
|
||||||
|
- hashWrting(`0b0100`): 一个goroutine正在向map中写入数据
|
||||||
|
- sameSizeGrow(`0b1000`): 等量扩容标志字段
|
||||||
|
|
||||||
|
## bmap
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> bmap就是map中的桶,可以存储8组key-value对数据,以及一个只想下一个溢出桶的指针
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
每一组key-value对数据包含key高8位hash值tophash,key,value三部分
|
||||||
|
|
||||||
|
我们来看看bmap(桶)的内存模型
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
如果按照 `key/value/key/value/...` 这样的模式存储,那在每一个 key/value 对之后都要额外 padding 7 个字节;而将所有的 key,value 分别绑定到一起,这种形式 `key/key/.../value/value/...`,则只需要在最后添加 padding。
|
||||||
|
|
||||||
|
每个 bucket 设计成最多只能放 8 个 key-value 对,如果有第 9 个 key-value 落入当前的 bucket,那就需要再构建一个 bucket ,通过 `overflow` 指针连接起来。
|
||||||
|
|
||||||
|
### tophash的作用?
|
||||||
|
|
||||||
|
是key 哈希值的高8位
|
||||||
|
|
||||||
|
tophash的核心作用是**判断一个键是否可能存在于当前桶中,从而优化查询效率。**
|
||||||
|
|
||||||
|
## 溢出桶数据结构 mapextra
|
||||||
|
|
||||||
|
在map初始化的时候会根据初始数据量不同,自动创建不同数量的溢出桶。在物理结构上初始的正常同和溢出桶是连续存放的,正常桶和溢出桶之间的关系是靠链表来维护的。
|
||||||
|
|
||||||
|
> `mapextra` 就是在扩容时提供了一批预备的 `bmap`,然后利用 `bmap.overflow` 把它们链接起来。
|
||||||
|
|
||||||
|
```go
|
||||||
|
type mapextra struct {
|
||||||
|
overflow *[]*bmap // overflow buckets 的指针数组
|
||||||
|
oldoverflow *[]*bmap // 旧的 overflow buckets 的指针数组
|
||||||
|
|
||||||
|
nextOverflow *bmap // 指向空闲的 overflow bucket
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
在map初始化的时候,倘若容量过大,会提前申请好一批溢出桶,供后续使用,这部分溢出桶存放在hmap.mapextra当中:
|
||||||
|
|
||||||
|
mapextra.overflow 是一个指向溢出桶切片的指针,这个切片里面的溢出桶是当前使用的,用于存储hmap.buckets中的桶的溢出数据。
|
||||||
|
|
||||||
|
mapextra.oldoverflow 也是一个指向溢出桶切片的指针,但是它指向的是旧的桶数组的溢出桶。
|
||||||
|
|
||||||
|
nextOverflow指向下一个可用的溢出桶
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 什么是哈希种子?
|
||||||
|
|
||||||
|
哈希种子(hash seed)是一个随机生成的数值,被用作哈希函数的一部分,来增加哈希值的随机性和不可预测性,可以把它理解为哈希函数的“盐”
|
||||||
|
|
||||||
|
# go map 如何根据key的哈希值确定键值存储到哪个桶中?
|
||||||
|
|
||||||
|
## 哈希值的作用
|
||||||
|
|
||||||
|
- 首先,当你在 Go map 中插入一个键值对时,Go runtime 会对键进行哈希运算,生成一个哈希值(一个整数)。 优秀的哈希函数应该能够将不同的键尽可能均匀地映射到不同的哈希值,以减少哈希碰撞的概率。
|
||||||
|
- 这个哈希值是确定键值对存储位置的关键。
|
||||||
|
|
||||||
|
## go map 数据结构中hmap 中B的作用
|
||||||
|
|
||||||
|
我们通过哈希值的低B位作为bucket数组的索引, 来选择键值该存储到哪个bucket中。
|
||||||
|
|
||||||
|
公式 `bucketIndex = hash & ((1 << B) - 1)`
|
||||||
|
|
||||||
|
上面的公式 用来**保留 `hash` 的低 `B` 位,并将其他位设置为 0**。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# key定位过程
|
||||||
|
|
||||||
|
key经过哈希计算后得到哈希值,共64个bit位,计算它到底要落在哪个桶的时候,只会用到最后B个bit位(log2BucketCount)
|
||||||
|
|
||||||
|
例如,现在有一个key经过哈希函数计算后,得到的哈希结果是:
|
||||||
|
|
||||||
|
```
|
||||||
|
10010111 | 000011110110110010001111001010100010010110010101010 │ 01010
|
||||||
|
```
|
||||||
|
|
||||||
|
而我们的B是5,也就是有2^5 = 32个桶
|
||||||
|
|
||||||
|
取最后五位,也就是 **01010** 转换为10进制也就是10,也就是 **10号桶**,这个操作其实是 **取余操作**,但是取余数开销太大,就用上面的位运算代替了。
|
||||||
|
|
||||||
|
接下来我们再用 **hash值的高8位**找到key在 **10号桶**中的位置 **1001011转换为10进制也就是 75**.最开始桶内还没有 key,新加入的 key 会找到第一个空位,放入。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# 流程
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# 写入流程
|
||||||
|
|
||||||
|
写入流程:
|
||||||
|
|
||||||
|
- 进行hmap是否为nil的检查,如果为空,就触发panic
|
||||||
|
- 进行并发读写的检查,倘若已经设置了并发读写标记,就抛出"concurrent map writes"异常。
|
||||||
|
- 处理桶迁移。如果正在扩容,把key所在的旧桶数据迁移到新桶,同时迁移index位h.nevacuate的桶,迁移完成后h.nevacuate自增。更新迁移进度。如果所有桶迁移完毕,清除正在扩容的标记。
|
||||||
|
- 查找 key 所在的位置,并记录桶链表的第一个空闲位置(若此 key 之前不存在,则将该位置作为插入位置)。
|
||||||
|
- 若此 key 在桶链表中不存在,判断是否需要扩容,若溢出桶过多,则进行相同容量的扩容,否则进行双倍容量的扩容。
|
||||||
|
- 若桶链表没有空闲位置,则申请溢出桶来存放 key - value 对。
|
||||||
|
- 设置 key 和 tophash[i] 的值。
|
||||||
|
- 返回 value 的地址。
|
||||||
|
|
||||||
|
# 删除流程
|
||||||
|
|
||||||
|
删除流程:
|
||||||
|
|
||||||
|
- 进行并发读写检查。
|
||||||
|
- 处理桶迁移,如果map处于正在扩容的状态,就迁移两个桶
|
||||||
|
- 定位key所在的位置
|
||||||
|
- 删除kv对的占用,这里是伪删除,只有在下次扩容的时候,被删除的key所占用的同空间才会得到释放。
|
||||||
|
- map首先会将对应位置的tophash[i]设置为emptyOne,表示该位置被删除
|
||||||
|
- 如果tophash[i]后面还有有效的节点,就仅设置为emptyOne标志,意味着这个节点后面仍然存在有效的key-value对 ,后续在查找某个key的时候,这个节点只后仍然需要继续查找
|
||||||
|
- 要是tophash[i]是桶链表的最后一个有效节点,那么从这个节点往前遍历,将链表最后面所有标志位emptyOne的位置,都设置为emptyRest。这样在查找某个key的时候,emptyRest之后的节点不需要继续查找。
|
||||||
|
|
||||||
|
> - **`emptyOne`:** 表示当前 cell 是空的,但**不能保证**后面的 cell 也是空的。
|
||||||
|
> - **`emptyRest`:** 表示当前 cell 是空的,并且**保证**后面的所有 cell 也是空的,直到遇到一个非空 cell 或者到达桶的末尾。
|
||||||
|
|
||||||
|
# 迭代流程
|
||||||
|
|
||||||
|
在每次对 map 进行循环时,会调用 mapiterinit 函数,以确定迭代从哪个桶以及桶内的哪个位置起始。由于 mapiterinit 内部是通过随机数来决定起始位置的,所以 map 循环是无序的,每次循环所返回的 key - value 对的顺序都各不相同。
|
||||||
|
|
||||||
|

|
||||||
189
src/content/posts/Golang/Go_slice切片原理.md
Normal file
189
src/content/posts/Golang/Go_slice切片原理.md
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
---
|
||||||
|
title: Go_slice切片原理
|
||||||
|
published: 2025-07-19
|
||||||
|
description: ''
|
||||||
|
image: 'https://blog.meowrain.cn/api/i/2025/07/19/uje4vo-1.webp'
|
||||||
|
tags: [切片, Golang, Go]
|
||||||
|
category: 'Go'
|
||||||
|
draft: false
|
||||||
|
lang: ''
|
||||||
|
---
|
||||||
|
# slice数据结构
|
||||||
|
|
||||||
|
数据结构
|
||||||
|
我们每定义一个slice变量,golang底层都会构建一个slice结构的对象。slice结构体由3个成员变量构成:
|
||||||
|
|
||||||
|
array表示数组指针,数组用于存储数据。
|
||||||
|
len表示切片长度,也就是数组index从0到len-1已存储数据。
|
||||||
|
cap表示切片容量,当切片长度超过最大容量时,需要扩容申请更大长度的数组。
|
||||||
|
|
||||||
|
```go
|
||||||
|
type slice struct {
|
||||||
|
array unsafe.Pointer // 数组指针
|
||||||
|
len int // 切片长度
|
||||||
|
cap int // 切片容量
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# 扩容原理
|
||||||
|
|
||||||
|
切片的扩容流程源码位于 runtime/slice.go 文件的 growslice 方法当中,其中核心步骤如下:
|
||||||
|
|
||||||
|
• 倘若扩容后预期的新容量小于原切片的容量,则 panic
|
||||||
|
|
||||||
|
• 倘若切片元素大小为 0(元素类型为 struct{}),则直接复用一个全局的 zerobase 实例,直接返回
|
||||||
|
|
||||||
|
• 倘若预期的新容量超过老容量的两倍,则直接采用预期的新容量
|
||||||
|
|
||||||
|
• 倘若老容量小于 256,则直接采用老容量的2倍作为新容量
|
||||||
|
|
||||||
|
• 倘若老容量已经大于等于 256,则在老容量的基础上扩容 1/4 的比例并且累加上 192 的数值,持续这样处理,直到得到的新容量已经大于等于预期的新容量为止
|
||||||
|
|
||||||
|
• 结合 mallocgc 流程中,对内存分配单元 mspan 的等级制度,推算得到实际需要申请的内存空间大小
|
||||||
|
|
||||||
|
• 调用 mallocgc,对新切片进行内存初始化
|
||||||
|
|
||||||
|
• 调用 memmove 方法,将老切片中的内容拷贝到新切片中
|
||||||
|
|
||||||
|
• 返回扩容后的新切片
|
||||||
|
|
||||||
|
```go
|
||||||
|
// nextslicecap computes the next appropriate slice length.
|
||||||
|
func nextslicecap(newLen, oldCap int) int {
|
||||||
|
newcap := oldCap // 将新容量初始化为旧容量
|
||||||
|
doublecap := newcap + newcap // 计算旧容量的两倍
|
||||||
|
|
||||||
|
// 如果所需的新长度大于旧容量的两倍,则直接使用所需的新长度
|
||||||
|
if newLen > doublecap {
|
||||||
|
return newLen
|
||||||
|
}
|
||||||
|
|
||||||
|
const threshold = 256 // 定义一个阈值,用于区分小切片和大切片
|
||||||
|
|
||||||
|
// 如果旧容量小于阈值,则直接将新容量设置为旧容量的两倍
|
||||||
|
// 这种策略适用于小切片,可以快速扩容,减少扩容次数
|
||||||
|
if oldCap < threshold {
|
||||||
|
return doublecap
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于大切片,使用更平滑的扩容策略,避免过度分配内存
|
||||||
|
// 从 2 倍增长过渡到 1.25 倍增长。 此公式给出了两者之间的平滑过渡。
|
||||||
|
for {
|
||||||
|
// 每次循环,将新容量增加 (newcap + 3*threshold) / 4
|
||||||
|
// 相当于 newcap 增加 1/4 的比例,再加上 3/4 的 threshold(256),即 192
|
||||||
|
// 这样可以在一定程度上减少内存浪费,并保证切片的增长
|
||||||
|
newcap += (newcap + 3*threshold) >> 2
|
||||||
|
|
||||||
|
// Check for overflow and determine if the new calculated capacity
|
||||||
|
// is greater or equal to the required new length.
|
||||||
|
// newLen is guaranteed to be larger than zero, hence
|
||||||
|
// when newcap overflows then `uint(newcap) > uint(newLen)`.
|
||||||
|
// This allows to check for both with the same comparison.
|
||||||
|
|
||||||
|
// 我们需要检查`newcap >= newLen`以及`newcap`是否溢出。
|
||||||
|
// 保证 newLen 大于零,因此当 newcap 溢出时,'uint(newcap) > uint(newLen)'。
|
||||||
|
// 这允许使用相同的比较来检查两者。
|
||||||
|
|
||||||
|
// 检查新容量是否大于等于所需的新长度,并且检查是否发生了溢出
|
||||||
|
if uint(newcap) >= uint(newLen) {
|
||||||
|
break // 如果新容量足够大,或者发生了溢出,则退出循环
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当新容量计算溢出时,将新容量设置为请求的容量。
|
||||||
|
// 如果计算过程中发生了溢出,则直接将新容量设置为所需的新长度,以确保切片能够容纳所有元素
|
||||||
|
if newcap <= 0 {
|
||||||
|
return newLen
|
||||||
|
}
|
||||||
|
|
||||||
|
return newcap // 返回计算得到的新容量
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Golang 切片原理
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 扩容规律
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 切片作为参数
|
||||||
|
|
||||||
|
Go 语言的函数参数传递,只有值传递,没有引用传递,切片作为参数也是如此
|
||||||
|
|
||||||
|
我们来验证这一点
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
sl := []int{6, 6, 6}
|
||||||
|
f(sl)
|
||||||
|
fmt.Println(sl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func f(sl []int) {
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
sl = append(sl, i)
|
||||||
|
}
|
||||||
|
fmt.Println(sl)
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
可以看到,输出的 sl 的值是不一样的,也就是说,f 函数没能修改主函数中的 sl 变量,而只是修改了形参 sl 变量的内容
|
||||||
|
|
||||||
|
当我们传递一个切片给函数的时候,函数接收到的其实是这个切片的一个副本,但是他们的 array 字段指向的是同一个底层数组。
|
||||||
|
|
||||||
|
这意味着,如果我们修改底层数组,是会影响到实参和形参的。
|
||||||
|
|
||||||
|
我们看下面的例子:形参通过改变底层数组影响实参
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
sl := []int{6, 6, 6}
|
||||||
|
f(sl)
|
||||||
|
fmt.Println(sl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func f(sl []int) {
|
||||||
|
sl[1] = 1
|
||||||
|
sl[2] = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 通过指针传递影响实参
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
sl := []int{6, 6, 6}
|
||||||
|
f(&sl)
|
||||||
|
fmt.Println(sl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func f(sl *[]int) {
|
||||||
|
*sl = append(*sl, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
244
src/content/posts/Golang/Golang垃圾回收机制.md
Normal file
244
src/content/posts/Golang/Golang垃圾回收机制.md
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
---
|
||||||
|
title: Golang垃圾回收机制
|
||||||
|
published: 2025-07-19
|
||||||
|
description: ''
|
||||||
|
image: 'https://blog.meowrain.cn/api/i/2025/07/19/uje4vo-1.webp'
|
||||||
|
tags: [垃圾回收, Golang, GC]
|
||||||
|
category: 'Go'
|
||||||
|
draft: false
|
||||||
|
lang: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
# Go GC机制
|
||||||
|
|
||||||
|
> [5、Golang三色标记混合写屏障GC模式全分析 (yuque.com)](https://www.yuque.com/aceld/golang/zhzanb#77fdf35b)
|
||||||
|
|
||||||
|
> 垃圾回收(Garbage Collection,简称GC)是编程语言中提供的自动的内存管理机制,自动释放不需要的内存对象,让出存储器资源。GC过程中无需程序员手动执行。GC机制在现代很多编程语言都支持,GC能力的性能与优劣也是不同语言之间对比度指标之一。
|
||||||
|
|
||||||
|
## 发展过程
|
||||||
|
|
||||||
|
Go V1.3之前的标记-清除(mark and sweep)算法,Go V1.3之前的标记-清扫(mark and sweep)的缺点
|
||||||
|
|
||||||
|
## Go V1.3之前的标记-清除(mark and sweep)算法
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
接下来我们来看一下在Golang1.3之前的时候主要用的普通的标记-清除算法,此算法主要有两个主要的步骤:
|
||||||
|
|
||||||
|
- 标记(Mark phase)
|
||||||
|
- 清除(Sweep phase)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> STW会对可达对象做上标记,然后对不可达对象进行GC回收
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> 操作非常简单,但是有一点需要额外注意:mark and sweep算法在执行的时候,需要程序暂停!即 `STW(stop the world)`,STW的过程中,CPU不执行用户代码,全部用于垃圾回收,这个过程的影响很大,所以STW也是一些回收机制最大的难题和希望优化的点。所以在执行第三步的这段时间,程序会暂定停止任何工作,卡在那等待回收执行完毕。
|
||||||
|
|
||||||
|
### mark and sweep 算法 缺点
|
||||||
|
|
||||||
|
1. STW会让程序暂停,使程序出现卡顿(重要问题)
|
||||||
|
2. 标记需要扫描整个heap
|
||||||
|
3. 清除数据会产生heap碎片
|
||||||
|
|
||||||
|
stw暂停范围
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
从上图来看,全部的GC时间都是包裹在STW范围之内的,这样貌似程序暂停的时间过长,影响程序的运行性能。所以Go V1.3 做了简单的优化,将STW的步骤提前, 减少STW暂停的时间范围.如下所示
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
上图主要是将STW的步骤提前了一步,因为在Sweep清除的时候,可以不需要STW停止,因为这些对象已经是不可达对象了,不会出现回收写冲突等问题。
|
||||||
|
|
||||||
|
但是无论怎么优化,Go V1.3都面临这个一个重要问题,就是**mark-and-sweep 算法会暂停整个程序** 。
|
||||||
|
|
||||||
|
Go是如何面对并这个问题的呢?接下来G V1.5版本 就用**三色并发标记法**来优化这个问题.
|
||||||
|
|
||||||
|
## GoV1.5三色标记法
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 三色标记法无STW的问题
|
||||||
|
|
||||||
|
我们加入如果没有STW,那么也就不会再存在性能上的问题,那么接下来我们假设如果三色标记法不加入STW会发生什么事情?
|
||||||
|
我们还是基于上述的三色并发标记法来说, 他是一定要依赖STW的. 因为如果不暂停程序, 程序的逻辑改变对象引用关系, 这种动作如果在标记阶段做了修改,会影响标记结果的正确性,我们来看看一个场景,如果三色标记法, 标记过程不使用STW将会发生什么事情?
|
||||||
|
|
||||||
|
我们把初始状态设置为已经经历了第一轮扫描,目前黑色的有对象1和对象4, 灰色的有对象2和对象7,其他的为白色对象,且对象2是通过指针p指向对象3的,如图所示。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
现在如何三色标记过程不启动STW,那么在GC扫描过程中,任意的对象均可能发生读写操作,如图所示,在还没有扫描到对象2的时候,已经标记为黑色的对象4,此时创建指针q,并且指向白色的对象3。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
与此同时灰色的对象2将指针p移除,那么白色的对象3实则就是被挂在了已经扫描完成的黑色的对象4下,如图所示。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
然后我们正常指向三色标记的算法逻辑,将所有灰色的对象标记为黑色,那么对象2和对象7就被标记成了黑色,如图所示。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
那么就执行了三色标记的最后一步,将所有白色对象当做垃圾进行回收,如图所示。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
但是最后我们才发现,本来是对象4合法引用的对象3,却被GC给“误杀”回收掉了。
|
||||||
|
|
||||||
|
### GC误杀条件
|
||||||
|
|
||||||
|
可以看出,有两种情况,在三色标记法中,是不希望被发生的。
|
||||||
|
|
||||||
|
- 条件1: 一个白色对象被黑色对象引用**(白色被挂在黑色下)**
|
||||||
|
- 条件2: 灰色对象与它之间的可达关系的白色对象遭到破坏**(灰色同时丢了该白色)**
|
||||||
|
如果当以上两个条件同时满足时,就会出现对象丢失现象!
|
||||||
|
|
||||||
|
## 屏障机制
|
||||||
|
|
||||||
|
> 为了防止这种现象的发生,最简单的方式就是STW,直接禁止掉其他用户程序对对象引用关系的干扰,但是**STW的过程有明显的资源浪费,对所有的用户程序都有很大影响**。那么是否可以在保证对象不丢失的情况下合理的尽可能的提高GC效率,减少STW时间呢?答案是可以的,我们只要使用一种机制,尝试去破坏上面的两个必要条件就可以了。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 强三色不变式
|
||||||
|
|
||||||
|
强制性的不允许黑色对象引用白色对象
|
||||||
|
|
||||||
|
> 破坏条件1
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 弱三色不变式
|
||||||
|
|
||||||
|
黑色对象可以引用白色对象,但是要保证白色独享存在其它灰色对象对它的引用,或者可达它的链路上游存在灰色对象
|
||||||
|
|
||||||
|
> 破坏条件2
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
为了遵循上述的两个方式,GC算法演进到两种屏障方式,他们“插入屏障”, “删除屏障”。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 插入屏蔽
|
||||||
|
|
||||||
|
> 不在栈上使用
|
||||||
|
|
||||||
|
`具体操作`: 在A对象引用B对象的时候,B对象被标记为灰色。(将B挂在A下游,B必须被标记为灰色)
|
||||||
|
|
||||||
|
`满足`: **强三色不变式**. (不存在黑色对象引用白色对象的情况了, 因为白色会强制变成灰色)
|
||||||
|
|
||||||
|
```go
|
||||||
|
添加下游对象(当前下游对象slot, 新下游对象ptr) {
|
||||||
|
//1
|
||||||
|
标记灰色(新下游对象ptr)
|
||||||
|
|
||||||
|
//2
|
||||||
|
当前下游对象slot = 新下游对象ptr
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这里说一下这个过程,首先因为插入屏障不在栈上使用
|
||||||
|
|
||||||
|
下面的图里面,已经进行了一次三色标记,外界向对象4添加对象8,对象1添加对象9,但是我们知道,对象1在栈上,所以它不会应用插入屏障,也就是说,这个时候对象 9不会按照插入屏障的规则设置为灰色,而对象4在堆上,因此它会应用插入屏障,所以会把对象8设置为灰色,然后我们进行第二次三色标记,从灰色对象出发(对象2,对象7,对象8) ,找可达对象(对象3),因此将对象3设置为灰色,然后对象2,7,8设置为黑色,接着进行第三次三色标记,从灰色对象出发(对象3),发现没有可达对象,因此设置对象3为黑色,这个时候我们有黑色对象: 对象1,对象2,对象3,对象4,对象7,对象8.
|
||||||
|
|
||||||
|
按照常理我们这个时候应该进行垃圾回收了对吧,其实不然,我们这个时候要把栈空间的对象全部设置为白色,然后使用STW暂停栈空间(对象1,对象2,对象3,对象9,对象5),防止外界干扰(再有对象被添加到黑色对象下)
|
||||||
|
|
||||||
|
然后我们对栈空间重新进行一次三色标记,直到没有灰色对象
|
||||||
|
|
||||||
|
过程如下:
|
||||||
|
|
||||||
|
从对象1出发,设置对象1为灰色,接下来看从对象1走的可达对象,发现可达对象有对象2和对象9,因此我们把对象2和对象9设置为灰色对象,把对象1设置为黑色对象,然后我们再从灰色对象出发(对象2和对象9),发现对象2可达对象3,对象9没有可达对象,因此把对象3设置为灰色对象,对象2,9设置为黑色对象,接下来从灰色对象(此时只有对象3)出发,发现对象3没有可达对象,设置对象3为黑色对象。至此栈里面已经没有灰色对象,我们先暂停STW,然后进行最后的GC回收,可以发现白色对象只有 对象5,对象6,因此对白色对象进行清除。
|
||||||
|
|
||||||
|
至此,GC三色标记并发情况下的插入屏障流程完毕
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 删除屏蔽
|
||||||
|
|
||||||
|
`具体操作`: 被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。
|
||||||
|
|
||||||
|
`满足`: **弱三色不变式**. (保护灰色对象到白色对象的路径不会断)
|
||||||
|
|
||||||
|
```
|
||||||
|
添加下游对象(当前下游对象slot, 新下游对象ptr) {
|
||||||
|
//1
|
||||||
|
if (当前下游对象slot是灰色 || 当前下游对象slot是白色) {
|
||||||
|
标记灰色(当前下游对象slot) //slot为被删除对象, 标记为灰色
|
||||||
|
}
|
||||||
|
|
||||||
|
//2
|
||||||
|
当前下游对象slot = 新下游对象ptr
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
这种方式的回收精度低,一个对象即使被删除了最后一个指向它的指针也依旧可以活过这一轮,在下一轮GC中被清理掉。
|
||||||
|
|
||||||
|
### 混合屏障Go V1.8
|
||||||
|
|
||||||
|
插入写屏障和删除写屏障的短板:
|
||||||
|
|
||||||
|
● 插入写屏障:结束时需要STW来重新扫描栈,标记栈上引用的白色对象的存活;
|
||||||
|
● 删除写屏障:回收精度低,GC开始时STW扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象。
|
||||||
|
|
||||||
|
Go V1.8版本引入了混合写屏障机制(hybrid write barrier),避免了对栈re-scan的过程,极大的减少了STW的时间。结合了两者的优点。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
`具体操作`:
|
||||||
|
|
||||||
|
1、GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW),
|
||||||
|
|
||||||
|
2、GC期间,任何在栈上创建的新对象,均为黑色。
|
||||||
|
|
||||||
|
3、被删除的对象标记为灰色。
|
||||||
|
|
||||||
|
4、被添加的对象标记为灰色。
|
||||||
@@ -11,7 +11,7 @@ lang: ''
|
|||||||
|
|
||||||
# 参考资料
|
# 参考资料
|
||||||
|
|
||||||

|
[万字图解| 深入揭秘IO多路复用](https://cloud.tencent.com/developer/article/2383534)
|
||||||
|
|
||||||
# 为什么要有IO多路复用技术?
|
# 为什么要有IO多路复用技术?
|
||||||
|
|
||||||
|
|||||||
2360
src/content/posts/设计模式/golang设计模式.md
Normal file
2360
src/content/posts/设计模式/golang设计模式.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user