• 关于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,这会导致迁移操作,如果可以避免,尽量避免

  • 相关阅读:
    解析Apache Kafka中的事务机制
    【C++初阶(五)类和对象(上)】
    AQS源码二探-JUC系列
    若依框架,集成flowable工作流
    在Gitlab平台及Jenkins平台中如何实现ci的过程
    「Redis数据结构」RedisObject
    【每日练习系列】—— 滑动窗口与数学模拟
    删库跑路、“投毒”、改协议,开源有哪几大红线千万不能踩?
    Linux 驱动开发 五十六:Buildroot 笔记
    网络ioctl实践3:设置网卡的mac、ip、子网掩码、广播地址
  • 原文地址:https://blog.csdn.net/m0_73728511/article/details/133621243