go 数据结构底层原理


array底层原理

go中的数组是由固定长度的特定类型元素组成的序列,数组的长度是数据类型的组成方式,所以不同长度和不同类型的元素组成的数组是不同的数组类型。数组属于值类型,因此在复制或者传递参数时,会对整个数组内容进行复制,所以在调用的函数中修改数组的值不会影响到原来的数组的值。此外由于需要复制整个数组的内容,如果数组太大会导致复制成本太大, 所以可以传递数组的指针。

Slice底层原理

简单的将,切片是一种简化版的动态数组。切片的在go中的定义为如下,在对切片赋值,就是修改指向数组的指针,len,cap的值。而在拷贝的时候,如果直接使用=,则会复制被拷贝的切片的数组指针,cap,len值,因此会指向同一个地址,而使用copy的话会把被拷贝的切片中的数组的值复制到拷贝的切片的数组中。即地址是不同的。

type SliceHeader struct{Data uintptrLen intCap int
}

Data可以简单看做指向了底层数组(其实其中Data保存了底层数组的指针的值,可以进行指针计算从而得到其他值,然后转变为Pointer,再转变为对应类型的指针进行访问。(uintptr不等于指针,只存储了底层数组的值,参考uintprt和pointer的区别))。len代表可以访问的长度,而Cap则是底层数组的容量(由可访问的第一个元素到底层数组末尾的长度),显然cap>=len,所以如果使用划分的方式的得到新切片时,如果从前面阶段,会导致cap减少,而从后面cap不会变.

a:=[]int{1,2,3,4,5} //len=5,cap=5
b:=a[1:3]//{2,3},len=2,cap=4
c:=a[:4]//{1,2,3,4},len=4,cap=5

Slice属于引用类型,简单来讲就是在调用函数中修改了slice中的值会影响到原来的切片中的值。因为golang是一个值传递的语言,在函数调用时候传递的参数时拷贝的副本。由上述复制原理可知,这里使用的是浅拷贝,及会把切片的值(指针,cap,len)复制一份,以此可以通过这个指针直接改变原切片的值,

参考资料1

参考资料2

但是如果在函数内部使用append扩容,首先如果cap足够,只需修改len的值,则此时函数里和函数外切片的指针值相同,但是外面len没有变,所以原来的切片并没有被扩容。而如果cap不够,进行扩容后会生成一个新的data数组,用于存储新的数据,这个时候,数组指针值,len,cap都不一样。所以对内部的修改不会影响到外面了。

map底层原理

参考资料

go中的map提供键值对形式的存储,属于引用类型,参数传递时其内部的指针被复制,指向同一个地址。所以函数内部的修改会影响到原来的map。map底层是散列表,通过拉链法(数组+链表)解决碰撞,在map扩容后不会立即替换原来的内存,只会慢慢通过GC释放

  • Golang的map本质上是一个指针,占用8个字节,指向了一个hmap结构体。
  • hmap中有一个字段是buckets,buckets是一个数组指针,指向由若干个bmap(bucket)组成的数组,数组的大小为2^B(B是桶的对数)。
  • bmap由四个字段组成:tophash、keys、values和overflow。每一个bmap最多可以存储8个元素(key、value对)。
  • 数据的存储机制:key经过哈希运算得到哈希值,哈希值的低B位决定了这个key进入哪一个bmap,哈希值的高8位决定key落入bmap的哪一个位置。当一个bmap存满之后,会创建一个新的bmap并通过链表连接

go map的底层结构:

// Map contains Type fields specific to maps.
type Map struct {Key  *Type // Key typeElem *Type // Val (elem) typeBucket *Type // internal struct type representing a hash bucketHmap   *Type // internal struct type representing the Hmap (map header object)Hiter  *Type // internal struct type representing hash iterator state
}

前两个字段分别为 key value, 由于 go map 支持多种数据类型, go 会在编译期推断其具体的数据类型, Bucket 是哈希桶, Hmap 表征了 map 底层使用的 HashTable 的元信息, 如当前 HashTable 中含有的元素数据、桶指针等, Hiter 是用于遍历 go map 的数据结构

Hmap低层结构如下:

type hmap struct {count     int    // 元素的个数,当使用len()返回的就是这个值flags     uint8  // 状态标志,遍历/写入B         uint8  // 可以最多容纳2 ^ B 个元素noverflow uint16 // 溢出的个数hash0     uint32 // 哈希种子,它能为哈希函数的结果引入随机性降低冲突率,这个值在创建哈希表时确定,并在调用哈希函数时作为参数传入buckets    unsafe.Pointer // 当前桶的地址oldbuckets unsafe.Pointer // 旧桶的地址,用于扩容,哈希在扩容时用于保存之前 buckets 的字段,它的大小是当前 buckets 的一半nevacuate  uintptr        // 搬迁进度,小于nevacuate的已经搬迁overflow *[2]*[]*bmap extra *mapextra // optional fields 表示溢出桶的变量,当key,value不为指针时有效,通过指向溢出桶的指针防止溢出桶被被gc
}

bmap的底层结构如下:

type bmap struct {topbits  [8]uint8 //键哈希值的高八位keys     [8]keytype//哈希桶中所有的建,最多8个elems    [8]elemtype//哈希桶里所有的值,最多8个//pad      uintptr(新的 go 版本已经移除了该字段, 之前设置该字段是为了在 nacl/amd64p32 上的内存对齐)overflow uintptr//存放了所指向的溢出桶的地址,当元素超过8个就会将新的元素放到溢出桶中, 并使用 overflow 指针链向这个溢出桶,
}

mapextra的底层结构如下:

type mapextra struct {// If both key and elem do not contain pointers and are inline, then we mark bucket// type as containing no pointers. This avoids scanning such maps.// However, bmap.overflow is a pointer. In order to keep overflow buckets// alive, we store pointers to all overflow buckets in hmap.extra.overflow and hmap.extra.oldoverflow.// overflow and oldoverflow are only used if key and elem do not contain pointers.// overflow contains overflow buckets for hmap.buckets.// oldoverflow contains overflow buckets for hmap.oldbuckets.// The indirection allows to store a pointer to the slice in hiter.overflow    *[]*bmapoldoverflow *[]*bmap// nextOverflow holds a pointer to a free overflow bucket.nextOverflow *bmap
}

当一个 map 的 key 和 elem 都不含指针并且他们的长度都没有超过 128 时(当 key 或 value 的长度超过 128 时, go 在 map 中会使用指针存储), 该 map 的 bucket 类型会被标注为不含有指针, 这样 gc 不会扫描该 map, 这会导致一个问题, bucket 的底层结构 bmap 中含有一个指向溢出桶的指针(uintptr类型, uintptr指针指向的内存不保证不会被 gc free 掉), 当 gc 不扫描该结构时, 该指针指向的内存会被 gc free 掉, 因此在 hmap 结构中增加了 mapextra 字段, 其中 overflow 是一个指向保存了所有 hmap.buckets 的溢出桶地址的 slice 的指针, 相对应的 oldoverflow 是指向保存了所有 hmap.oldbuckets 的溢出桶地址的 slice 的指针, 只有当 map 的 key 和 elem 都不含指针时这两个字段才有效, 因为这两个字段设置的目的就是避免当 map 被 gc 跳过扫描带来的引用内存被 free 的问题, 当 map 的 key 和 elem 含有指针时, gc 会扫描 map, 从而也会获知 bmap 中指针指向的内存是被引用的, 因此不会释放对应的内存。

hmap 结构相当于 go map 的头, 它存储了哈希桶的内存地址, 哈希桶之间在内存中紧密连续存储, 彼此之间没有额外的 gap, 每个哈希桶最多存放 8 个 k/v 对, 冲突次数超过 8 时会存放到溢出桶中, 哈希桶可以跟随多个溢出桶, 呈现一种链式结构, 当 HashTable 的装载因子超过阈值(6.5) 后会触发哈希的扩容, 避免效率下降

  • 增删查改

哈希函数是哈希表的特点之一,通过 key 值计算哈希,快速映射到数据的地址. golang 的 map 进行哈希计算后,将结果分为高位值和低位值,其中低位值用于定位 buckets 数组中的具体 bucket,而高位值用于定位这个 bucket 链表中具体的 key .

  • 扩容

当插入的元素越来越多,导致哈希桶慢慢填满,导致溢出桶越来越多,所以发生哈希碰撞的频率越来越高,就需要进行扩容,

若装载因子过大, 说明此时 map 中元素数目过多, 此时 go map 的扩容策略为将 hmap 中的 B 增一, 即将整个哈希桶数目扩充为原来的两倍大小, 而当因为溢出桶数目过多导致扩容时, 因此时装载因子并没有超过 6.5, 这意味着 map 中的元素数目并不是很多, 因此这时的扩容策略是等量扩容, 即新建完全等量的哈希桶, 然后将原哈希桶的所有元素搬迁到新的哈希桶中。

  • key的选择

    slice,map,包含了slice的function和struct不能够作为Map的Key,而数字,string,bool,数组,channel,指针都可以作为key。这是因为map的key必须是可以比较的(可以使用==运算符的称为可比较),而slice和map只能与nil比较。参考资料

channel底层原理

参考资料1

参考资料2

channel是golang中用来实现多个goroutine通信的管道,它的底层是一个叫做hchan的结构体。在go的runtime包下。

type hchan struct {//channel分为无缓冲和有缓冲两种。//对于有缓冲的channel存储数据,借助的是如下循环数组的结构qcount   uint           // 循环数组中的元素数量dataqsiz uint           // 循环数组的长度buf      unsafe.Pointer // 指向底层循环数组的指针elemsize uint16 //能够收发元素的大小closed   uint32   //channel是否关闭的标志elemtype *_type //channel中的元素类型//有缓冲channel内的缓冲数组会被作为一个“环型”来使用。//当下标超过数组容量后会回到第一个位置,所以需要有两个字段记录当前读和写的下标位置sendx    uint   // 下一次发送数据的下标位置recvx    uint   // 下一次读取数据的下标位置//当循环数组中没有数据时,收到了接收请求,那么接收数据的变量地址将会写入读等待队列//当循环数组中数据已满时,收到了发送请求,那么发送数据的变量地址将写入写等待队列recvq    waitq  // 读等待队列sendq    waitq  // 写等待队列lock mutex //互斥锁,保证读写channel时不存在并发竞争问题
}

Go 语言中,不要通过共享内存来通信,而要通过通信来实现内存共享。Go 的CSP(Communicating Sequential Process)并发模型,中文可以叫做通信顺序进程,是通过 goroutine 和 channel 来实现的。 加入ch时长度为4带缓冲的channel,G1是发送者,G2是接收者:

初始hchan结构体重的buf为空,sendx和recvx均为0。

当G1向ch里发送数据时,首先会对buf加锁,然后将数据copy到buf中,然后sendx++,然后释放对buf的锁。

当G2消费ch的时候,会首先对buf加锁,然后将buf中的数据copy到task变量对应的内存里,然后recvx++,并释放锁。

可以发现整个过程,G1和G2没有共享的内存,底层是通过hchan结构体的buf,并使用copy内存的方式进行通信,最后达到了共享内存的目的,这里也体现了Go中的CSP并发模型。

  • 无缓冲channel读写

先读后写:由于channel是无缓冲的,G1(读goroutine)会被挂在recvq队列,然后休眠。当G2(写goroutine)写入数据时,发现recvq队列中的G1,就会将数据传给G1,并设置G1 goready函数,等待下次调度运行,同时会将G1从等待队列中移出。

先写后读:由于channel是无缓冲的,因此G1(写goroutine)会被挂在sendq队列,然后休眠。当G2(读goroutine)来读数据时,发现sendq队列中的G1,将G1的数据取出来,并对G1设置goready函数,这样下次再发生调度时,G1就可以正常运行,并且会从等待队列中移除。

  • 有缓冲channel

先读再写:先判断缓冲区是否有数据,如果有数据则从缓冲区取数据,取完数据之后如果sendq队列中有数据则会按序将sendq队列中的数据放入缓冲区尾部。如果没有数据则将G1(读goroutine)保存在recevq队列,并且休眠。当G2(写goroutine)写数据时,为了提高效率,runtime并不会对hchan结构体题的buf进行加锁,而是直接将G2里的发送到ch的数据copy到了G1sudog里对应的elem指向的内存地址!【不通过buf】

先写再读:先判断缓冲区是否已满,如果未满则将G1(写goroutine)保存在缓冲区,如果已满则将G1挂在sendq队列,并且休眠。当G2(读goroutine)读数据时,优先去缓冲区取数据,如果缓冲区没有数据则挂在recevq队列,并且休眠。当G2取完数据之后如果sendq队列中有数据则会按序将sendq队列中的数据放入缓冲区尾部。

为什么在第一种情况下,即G1向缓存满的channel中发送数据时被阻塞。在G2后来接收时,不将阻塞的G1发送的数据直接拷贝到G2中呢?

这是因为channel中的数据是队列的,遵循先进先出的原则,当有消费者G2接收数据时,需要先接收缓存中的数据,即buf中的数据,而不是直接消费阻塞的G1中的数据。

  • Channel为什么是线程安全的?

    在对buf中的数据进行入队和出队操作时,为当前chnnel使用了互斥锁,防止多个线程并发修改数据

  • buffer实现

channel中使用了 ring buffer(环形缓冲区) 来缓存写入的数据。ring buffer 有很多好处,而且非常适合用来实现 FIFO 式的固定长度队列。

【Go语言学习】——go 数据结构底层原理相关推荐

  1. python常用代码_Python常用算法学习(4) 数据结构(原理+代码)-最全总结

    数据结构简介 1,数据结构 数据结构是指相互之间存在着一种或多种关系的数据元素的集合和该集合中数据元素之间的关系组成.简单来说,数据结构就是设计数据以何种方式组织并存贮在计算机中.比如:列表,集合与字 ...

  2. long 转为string_面试必问 Redis数据结构底层原理String、List篇

    点击关注上方"Java大厂面试官",第一时间送达技术干货. 阅读文本大概需要 8 分钟. 前言 今天来整理学习下Redis有哪些常用数据结构,都是怎么使用的呢?首先看下全局存储结构 ...

  3. C语言指针:从底层原理到花式技巧,用图文和代码帮你讲解透彻

    一.前言 二.变量与指针的本质 三.指针的几个相关概念 四.指向不同数据类型的指针 五.总结 一.前言 如果问C语言中最重要.威力最大的概念是什么,答案必将是指针!威力大,意味着使用方便.高效,同时也 ...

  4. 唤醒手腕Python全栈工程师学习笔记(底层原理篇)

    01.内建名称空间 在Python中,有一个内建模块,该模块中有一些常用函数,变量和类. 而该内建模块在Python启动后.且没有执行程序员所写的任何代码前,Python首先会自动加载该内建模块到内存 ...

  5. 6.X elasticsearch实战学习笔记_底层原理

    一.倒排索引 倒排索引 词项(term) : 搜索时的一个单位,代表文本中某个词 倒排索引结果是一种将词项映射得到文档的数据结构 倒排索引建立步骤 a. 提取词项 首先对文档分词,英文文档用空格分隔 ...

  6. Go语言潜力有目共睹,但它的Goroutine机制底层原理你了解吗?

    来源 | 后端技术指南针(ID:gh_ed1e2b37dcb6) Go语言的巨大潜力有目共睹,今天我们来学习Go语言的Goroutine机制,这也可能是Go语言最为吸引人的特性了,理解它对于掌握Go语 ...

  7. golang goroutine实现_Go语言潜力有目共睹,但它的Goroutine机制底层原理你了解吗?...

    来源 | 后端技术指南针(ID:gh_ed1e2b37dcb6) Go语言的巨大潜力有目共睹,今天我们来学习Go语言的Goroutine机制,这也可能是Go语言最为吸引人的特性了,理解它对于掌握Go语 ...

  8. 再谈js对象数据结构底层实现原理-object array map set

    2019独角兽企业重金招聘Python工程师标准>>> 如果有java基础的同学,可以回顾下<再谈Java数据结构-分析底层实现与应用注意事项>:java把内存分两种:一 ...

  9. Golang底层原理学习笔记(一)

    LCY~~Golang底层原理学习笔记 1 源码调试 go源代码地址:GitHub - golang/go: The Go programming language 1.1 源码编译 现在的go语言大 ...

最新文章

  1. leetcode--下一个更大元素II--python
  2. 【滴滴专场】深度学习模型优化技术揭秘
  3. ML之SVM:基于sklearn的svm算法实现对支持向量的数据进行标注
  4. Mysql 5.5 源码安装
  5. 跳一跳201803-1
  6. 保留3位 python_Python基础(六)
  7. 20 张图揭开内存管理的迷雾,瞬间豁然开朗
  8. fast.ai 深度学习笔记:第一部分第四课
  9. 每秒几十万的大规模网络爬虫是如何炼成的?
  10. 铭飞MCMS内容管理系统完整开源版J2EE代码
  11. python dataframe将字符转换为数字_python中如何将华氏温度转换为摄氏温度?
  12. 用u盘安装黑苹果10.12.3
  13. 数据库实例: STOREBOOK 用户
  14. 递归神经网络的非零初始状态
  15. DIV_ROUND_CLOSEST函数
  16. 【亲测有效】macOS无法验证此App不包含恶意软件
  17. 搬运视频抖音封号md5视频修改工具
  18. java加载mysql驱动_Java 加载数据库驱动(JDBC)
  19. 开发转测试? Yes or No
  20. lvds输入悬空_LVDS技术原理及详细介绍

热门文章

  1. ionic中的slide-box
  2. 沃信科技T3 Sota安装配置手册(1-4章)
  3. 【爱情叙记】--刚闹完别扭
  4. 科幻.后现代.后人类
  5. PMP/高项 项目管理培训大纲
  6. 安装R包的几种方法(汇总)
  7. java tomcat 404配置_在Tomcat中配置404自定义错误页面详解
  8. 《深入理解计算机系统》之浅析程序性能优化
  9. 从工具了解大数据之Kettle
  10. 打卡赠书的几点重要说明