Go 语言常见知识点总结

2023-08-06 15:29:30 阅读:823 评论:0 点赞:0
所属分类: Go 语言学习笔记

切片(Slice)底层实现原理

Go 语言中的 Slice 是一种动态大小的数据结构,其底层实现包含三个指针,分别指向数据、长度和容量。当我们创建一个 Slice 时,实际上会分配一个底层的数组来存储数据,同时使用三个指针来管理这个数组。但是,这个底层数组的长度和容量是可以动态调整的,而Slice的长度和容量也可以通过切片操作来动态改变。
967bfa92-4bad-4f99-a061-69cbedbe8008
数据指针:指向存储 Slice 数据底层数组的内存地址。
长度:表示 Slice 中元素的个数。
容量:表示 Slice 可容纳的元素数量。

扩容规则
如果切片的容量小于 1024 个元素,那么扩容的时候 slice 的 cap 就翻番,乘以2;一旦元素个数超过 1024 个元素,增长因子就变成 1.25,即每次增加原来容量的四分之一。

扩容影响
如果扩容之后,还没有触及原数组的容量,那么,切片中的指针指向的位置,就还是原数组,如果扩容之后,超过了原数组的容量,那么,Go就会开辟一块新的内存,把原来的值拷贝过来,这种情况丝毫不会影响到原数组。

切片(Slice)和数组的区别

Slice 是一个动态大小的数据结构,可以根据需要自动调整大小,而数组则是固定大小的数据结构,大小一旦确定就不能改变。slice 底层数据是一个结构体,包含三个元素,长度、容量和数组指针,slice 切片本身是一个只读对象,可以看作是对数组的一种封装。
967bfa92-4bad-4f99-a061-69cbedbe8008

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 进入睡眠,等待其他 GoroutineChannel 接收数据时唤醒。

④ 通道已关闭
如果 Channel 已经关闭,那么向该 Channel 发送数据时会报 send on closed channel 错误并中止程序。

接收数据

① 直接接收
当存在等待的发送者时,即 sendq 不为空(表示存在因为发送数据而阻塞的 Goroutine 列表,即此时缓冲区没有或者已满)

  • 不存在缓冲区,则从发送队列 sendq 中取出最先陷入等待的 Goroutine 将数据拷贝到目标的内存地址。
  • 存在缓冲区,将缓冲区首部的数据拷贝到接收方的内存地址,再将发送队列头的数据拷贝到缓冲区中,释放一个阻塞的发送方;

若存在缓冲区,则先从缓冲区首部获取数据,再将 sendq 中取出最先陷入等待的 Goroutine 的数据放入缓冲区尾部。

② 缓冲区
当缓冲区存在数据时,从 Channel 的缓冲区中接收数据;

③ 阻塞接收
不存在缓冲区或者缓冲区已满时,会将当前 Goroutine 加入 recvq 进入睡眠,等待其他 GoroutineChannel 发送数据时唤醒。

④ 通道已关闭
如果当前 Channel 已经被关闭并且缓冲区中不存在任何数据

Context 结构原理

Context(上下文)是 Golang 应用开发常用的并发控制技术 ,它可以控制一组呈树状结构的 goroutine,每个 goroutine 拥有相同的上下文。Context 是并发安全的,主要是用于控制多个协程之间的协作、取消操作。
042a77e6-02b6-487c-b676-36ccf3193439
数据结构如下:

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 来获取对应的值。

不拘一格

职业:后端开发工程师
学校:重庆师范大学
城市:重庆
文章:165
一个喜欢学习的人,快来和我成为朋友吧....

登录逐梦笔记

注册逐梦笔记

已有账号?