关于Go语言的底层,Slice,map

1 Slice

Slice底层实现原理

切片是基于数组实现的,它的底层是数组,它自己本身非常小,可以理解为对底层数组的抽象。因为基于数组实现,所以它的底层的内存是连续分配的,效率非常高,还可以通过索引获得数据,可以迭代以及垃圾回收优化。 切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一 个只读对象,其工作机制类似数组指针的一种封装。

切片对象非常小,是因为它是只有 3 个字段的数据结构:

  • 指向底层数组的指针
  • 切片的长度
  • 切片的容量

Slice扩容机制

在使用 append 向 slice 追加元素时,若 slice 空间不足则会发生扩容,扩容会重新分配一块更大的内存,将原 slice 拷贝到新 slice ,然后返回新 slice。扩容后再将数据追加进去。

扩容操作只对容量,扩容后的 slice 长度不变,容量变化规则如下:

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

若 slice 容量够用,则将新元素追加进去,slice.len++,返回原 slice

若 slice 容量不够用,将 slice 先扩容,扩容得到新 slice,将新元素追加进新 slice,slice.len++,返回新 slice。

Slice与数组区别

array是固定长度的数组,使用前必须确定数组长度,是值类型。

slice是一个引用类型,是一个动态的指向数组切片的指针。

slice是一个不定长的,总是指向底层的数组array的数据结构,可以动态扩容。

创建方式不一样,Slice使用make创建或者根据数组创建。

作为函数参数时,数组传递的是数组的副本,而slice传递的是指针。

2 Map

Map底层实现原理

Golang 中 map 的底层实现是一个散列表,因此实现 map 的过程实际上就是实现散表的过程。在这个散列表中,主要出现的结构体有两个,一个叫 hmap(a header for a go map),一个叫 bmap(a bucket for a Go map,通常叫其 bucket)。

hmap 哈希表

hmap是Go map的底层实现,每个hmap内都含有多个bmap(buckets桶、oldbuckets旧桶、overflow溢出桶),既每个哈希表都由多个桶组成。

  • buckets buckets是一个指针,指向一个bmap数组,存储多个桶。
  • oldbuckets oldbuckets是一个指针,指向一个bmap数组,存储多个旧桶,用于扩容。
  • overflow
    overflow是一个指针,指向一个元素个数为2的数组,数组的类型是一个指针,指向一个slice,slice的元素是桶(bmap)的地址,这些桶都是溢出桶。为什么有两个?因为Go
    map在哈希冲突过多时,会发生扩容操作。[0]表示当前使用的溢出桶集合,[1]是在发生扩容时,保存了旧的溢出桶集合。overflow存在的意义在于防止溢出桶被gc。
  • bmap 哈希桶
    bmap是一个隶属于hmap的结构体,一个桶(bmap)可以存储8个键值对。如果有第9个键值对被分配到该桶,那就需要再创建一个桶,通过overflow指针将两个桶连接起来。在hmap中,多个bmap桶通过overflow指针相连,组成一个链表。

Map进行有序的排序

map每次遍历,都会从一个随机值序号的桶,再从其中随机的cell开始遍历,并且扩容后,原来桶中的key会落到其他桶中,本身就会造成失序

如果想顺序遍历map,先把key放到切片排序,再按照key的顺序遍历map。

或者可以先把map中的key,通过sort包排序,再遍历map。

map 为什么是不安全的

Go map 默认是并发不安全的,同时对 map 进行并发读写的时,程序会 panic,原因如下:Go 官方经过长时间的讨论,认为 map 适配的场景应该是简单的(不需要从多个 gorountine 中进行安全访问的),而不是为了小部分情况(并发访问),导致大部分程序付出锁的代价,因此决定了不支持。

map 在扩缩容时,需要进行数据迁移,迁移的过程并没有采用锁机制防止并发操作,而是会对某个标识位标记为 1,表示此时正在迁移数据。如果有其他 goroutine 对 map 也进行写操作,当它检测到标识位为 1 时,将会直接 panic。

如果想实现map线程安全,有两种方式:

方式一:使用读写锁 map + sync.RWMutex

方式二:使用golang提供的 sync.Map

Map扩容策略

扩容时机:

向 map 插入新 key 的时候,会进行条件检测,符合下面这 2 个条件,就会触发扩容

扩容条件:

超过负载 map元素个数 > 6.5(负载因子) * 桶个数

溢出桶太多

当桶总数<2^15时,如果溢出桶总数>=桶总数,则认为溢出桶过多

当桶总数>215时,如果溢出桶总数>=215,则认为溢出桶过多

扩容机制:

双倍扩容:针对条件1,新建一个buckets数组,新的buckets大小是原来的2倍,然后旧buckets数据搬迁到新的buckets。

等量扩容:针对条件2,并不扩大容量,buckets数量维持不变,重新做一遍类似双倍扩容的搬迁动作,把松散的键值对重新排列一次,使得同一个 bucket 中的 key 排列地更紧密,节省空间,提高 bucket 利用率,进而保证更快的存取。

渐进式扩容:

插入修改删除key的时候,都会尝试进行搬迁桶的工作,每次都会检查oldbucket是否nil,如果不是nil则每次搬迁2个桶,蚂蚁搬家一样渐进式扩容

Map和Slice区别

数组:数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。声明方式:var a [3]int

slice(切片):Slice(切片)代表变长的序列,序列中每个元素都有相同的类型,slice的语法和数组很像,只是没有固定长度而已。

map:在Go语言中,一个map就是一个哈希表的引用,是一个无序的key/value对的集合

Map总结

map是引用类型

map遍历是无序的

map是非线程安全的

map的哈希冲突解决方式是链表法

map的扩容不是一定会新增空间,也有可能是只是做了内存整理

map的迁移是逐步进行的,在每次赋值时,会做至少一次迁移工作

map中删除key,有可能导致出现很多空的kv,这会导致迁移操作,如果可以避免,尽量避免

相关推荐
天天摸鱼的java工程师7 分钟前
2025已过半,Java就业大环境究竟咋样了?
java·后端
人生在勤,不索何获-白大侠12 分钟前
day16——Java集合进阶(Collection、List、Set)
java·开发语言
货拉拉技术12 分钟前
OceanBase向量检索在货拉拉的探索和实践
后端
LIN-JUN-WEI30 分钟前
[ESP32]VSCODE+ESP-IDF环境搭建及blink例程尝试(win10 win11均配置成功)
c语言·开发语言·ide·vscode·单片机·学习·编辑器
转转技术团队31 分钟前
多代理混战?用 PAC(Proxy Auto-Config) 优雅切换代理场景
前端·后端·面试
南囝coding32 分钟前
这几个 Vibe Coding 经验,真的建议学!
前端·后端
阿杆34 分钟前
服务一挂就手忙脚乱?教你用 Amazon Lambda 打造 0 成本服务监控!
后端·自动化运维
德育处主任1 小时前
在亚马逊云上,如何基于 VPC IPAM 的 ALB 公网 IP 预测分配?
后端
望获linux1 小时前
【Linux基础知识系列】第四十三篇 - 基础正则表达式与 grep/sed
linux·运维·服务器·开发语言·前端·操作系统·嵌入式软件
不吃肉的羊1 小时前
PHP设置文件上传最大值
后端·php