切片(Slice)底层实现原理
Go 语言中的 Slice
是一种动态大小的数据结构,其底层实现包含三个指针,分别指向数据、长度和容量。当我们创建一个 Slice 时,实际上会分配一个底层的数组来存储数据,同时使用三个指针来管理这个数组。但是,这个底层数组的长度和容量是可以动态调整的,而Slice的长度和容量也可以通过切片操作来动态改变。
数据指针:指向存储 Slice 数据底层数组的内存地址。
长度:表示 Slice 中元素的个数。
容量:表示 Slice 可容纳的元素数量。
扩容规则
如果切片的容量小于 1024 个元素,那么扩容的时候 slice 的 cap
就翻番,乘以2;一旦元素个数超过 1024 个元素,增长因子就变成 1.25,即每次增加原来容量的四分之一。
扩容影响
如果扩容之后,还没有触及原数组的容量,那么,切片中的指针指向的位置,就还是原数组,如果扩容之后,超过了原数组的容量,那么,Go就会开辟一块新的内存,把原来的值拷贝过来,这种情况丝毫不会影响到原数组。
切片(Slice)和数组的区别
Slice
是一个动态大小的数据结构,可以根据需要自动调整大小,而数组则是固定大小的数据结构,大小一旦确定就不能改变。slice 底层数据是一个结构体,包含三个元素,长度、容量和数组指针,slice 切片本身是一个只读对象,可以看作是对数组的一种封装。
Map 底层实现原理
Go 中的 map 底层实现原理是基于哈希表的。Go 语言运行时同时使用了多个数据结构组合表示哈希表,其核心的数据结构为 runtime.hmap
表示:
type hmap struct {
count int // 哈希表中的元素数量
flags uint8
B uint8 // 哈希表持有的 buckets 数量
noverflow uint16
hash0 uint32
// 是哈希的种子,它能为哈希函数的结果引入随机性,
// 这个值在创建哈希表时确定,并在调用哈希函数时作为参数传入
buckets unsafe.Pointer
oldbuckets unsafe.Pointer // 是哈希在扩容时用于保存之前 buckets 的字段,它的大小是当前 buckets 的一半
nevacuate uintptr
extra *mapextra
}
type mapextra struct {
overflow *[]*bmap
oldoverflow *[]*bmap
nextOverflow *bmap
}
// bucket
type bmap struct {
topbits [8]uint8
keys [8]keytype
values [8]valuetype
pad uintptr
overflow uintptr
}
初始化
当创建一个map时,Go语言会为这个map分配一块内存空间,并在这个内存空间中创建一个哈希表。这个哈希表包含了一个桶数组 buckets
和一些元数据,比如哈希函数和键值对的数量等。
插入
当我们向map中插入一个键值对时,Go语言会首先计算这个键的哈希值,然后将这个键值对插入到哈希表中对应桶中。如果这个桶中已经存在了一个相同键,那么就会将这个键值对替换掉原来的键值对。
提示
Golang 把求得的哈希值按照用途分为高位和低位。低位用于寻找当前 key
属于 hmap
中的哪个 bucket
,而高位用于寻找 bucket
中的哪个 key
。
扩容
当
map
的装载因子超过6.5
时或者溢出桶太多时将触发扩容。
当 map
的装载因子超过 6.5
时会触发扩容操作,每次都会将桶的数量翻倍,扩容过程不是原子的,而是通过 runtime.growWork
增量触发的,在扩容期间访问哈希表时会使用旧桶,向哈希表写入数据时会触发旧桶元素的分流。
除了这种正常的扩容之外,为了解决大量写入、删除造成的内存泄漏问题,哈希引入了等量扩容 sameSizeGrow
这一机制,在出现较多溢出桶时会整理哈希的内存减少空间的占用,即溢出桶的数量大于等于 2^B
这个阈值时将触发等量扩容。
提示
map
容量设置过小或者数据插入不均匀都可能会导致过多的溢出桶。
runtime.hashGrow
函数是扩容的入口,在该函数中仅创建新的哈希表和设置相关的属性,而真正的数据迁移工作在后续的 growWork()
和 evacuate()
函数中完成。
查找
当我们从map中查找一个键时,Go语言会首先计算这个键的哈希值,然后在哈希表中对应桶中查找对应的值。如果这个桶中不存在对应的值,那么就说明这个键不存在于map中。
删除
当我们从map中删除一个键时,Go语言会首先计算这个键的哈希值,然后在哈希表中对应桶中删除对应的值。如果这个桶中不存在对应的值,那么就说明这个键不存在于map中。
如何对 map 进行排序
map
是无序的,我们可以将键保存到一个单独的数组中然后通过 sort
方法进行排序,例如:
func main() {
var data = make(map[string]int)
data["stan"] = 75
data["alice"] = 250
data["jack"] = 100
var keys []string
for k := range data {
keys = append(keys, k)
}
sort.Strings(keys)
for _, key := range keys {
fmt.Printf("%s:%d ", key, data[key])
}
}
// alice:250 jack:100 stan:75
Channel 底层原理
Go 语言的 Channel
在运行时使用 runtime.hchan
结构体表示。我们在 Go 语言中创建新的 Channel
时,实际上创建的都是如下所示的结构:
type hchan struct {
qcount uint // Channel 中的元素个数
dataqsiz uint // Channel 中的循环队列的长度;
buf unsafe.Pointer // Channel 的缓冲区数据指针;
elemsize uint16 // Channel 能够收发的元素大小
closed uint32
elemtype *_type // Channel 能够收发的元素类型
sendx uint // Channel 的发送操作处理到的位置;
recvx uint // Channel 的接收操作处理到的位置;
recvq waitq // 存储了从当前 Channel 接收数据由于缓冲区空间不足而阻塞的 Goroutine 列表
sendq waitq // 存储了从当前 Channel 发送数据由于缓冲区空间不足而阻塞的 Goroutine 列表
lock mutex
}
在操作 Channel 前会先加锁,防止多个线程并发修改数据。
发送数据
① 直接发送
当存在等待的接收者时,即 recvq
不为空(表示存在因为接收数据而阻塞的 Goroutine 列表,且缓冲区中无数据或无缓冲区),则从接收队列 recvq
中取出最先陷入等待的 Goroutine
并直接向它发送数据(recvx
所在位置)。
② 缓冲区
当缓冲区存在空余空间时,将发送的数据写入 Channel 的缓冲区;
③ 阻塞发送
不存在缓冲区或者缓冲区已满时,会将当前 Goroutine 加入 sendq
进入睡眠,等待其他 Goroutine
从 Channel
接收数据时唤醒。
④ 通道已关闭
如果 Channel
已经关闭,那么向该 Channel
发送数据时会报 send on closed channel
错误并中止程序。
接收数据
① 直接接收
当存在等待的发送者时,即 sendq
不为空(表示存在因为发送数据而阻塞的 Goroutine 列表,即此时缓冲区没有或者已满)
- 不存在缓冲区,则从发送队列
sendq
中取出最先陷入等待的Goroutine
将数据拷贝到目标的内存地址。 - 存在缓冲区,将缓冲区首部的数据拷贝到接收方的内存地址,再将发送队列头的数据拷贝到缓冲区中,释放一个阻塞的发送方;
若存在缓冲区,则先从缓冲区首部获取数据,再将 sendq
中取出最先陷入等待的 Goroutine
的数据放入缓冲区尾部。
② 缓冲区
当缓冲区存在数据时,从 Channel
的缓冲区中接收数据;
③ 阻塞接收
不存在缓冲区或者缓冲区已满时,会将当前 Goroutine 加入 recvq
进入睡眠,等待其他 Goroutine
从 Channel
发送数据时唤醒。
④ 通道已关闭
如果当前 Channel 已经被关闭并且缓冲区中不存在任何数据
Context 结构原理
Context
(上下文)是 Golang 应用开发常用的并发控制技术 ,它可以控制一组呈树状结构的 goroutine,每个 goroutine 拥有相同的上下文。Context 是并发安全的,主要是用于控制多个协程之间的协作、取消操作。
数据结构如下:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
Deadline
可以获取设置的截止时间,返回值 deadline 是截止时间,到了这个时间,Context 会自动发起取消请求,返回值 ok 表示是否设置了截止时间。
Done
返回一个只读的 channel ,类型为 struct{}
。如果这个 chan 可以读取,说明已经发出了取消信号,可以做清理操作,然后退出协程,释放资源。
Err
返回 Context 被取消的原因。
Value
获取 Context 上绑定的值,是一个键值对,通过 key 来获取对应的值。