slice
首先让我们了解一下slice的底层数据结构
type slice struct {
array unsafe.Pointer 指向一个指针数组
len int 长度
cap int 容量
}
array的类型是unsafe.Pointer指针,指向了一个数组的内存地址
len 代表了当前数组的长度
cap 代表了当前数组的容量
接下来我们介绍一下如何创建slice
s := make([]int,4,10)
定义一个数组0-4填充为0,长度容量为10的slice切片
根据结构中可以看出来,array是个指针,所以这时用一个变量继承这个数组 例如:
a := s[3,4]
那么这时 a和s中array指向的内存空间实际上是一块内存,也就是说我此时操作 a = append(a,9),那么s变量中也会发生改变
那么什么时候a改变了后s不会发生同步改变呢?
答案就是a从下表3开始,往后追加,追加到s的长度不足以满足需求后,那么这时候a就会自己开辟内存空间了
slice的是如何扩容的呢?
当空间不够后,slice会翻倍扩容,然后将原内存中的值copy到新的内存空间中
如果slice当作参数传递到另外一个函数中,那么此时如果函数对slice做出修改,那么外部的slice也会修改,如果做了追加外部的slice则不会继续追加
这是为什么呢?
因为slice虽然是指传递,但是他的array实际上是指针,也就是说两个slice同时指向了同一个内存地址,所以slice的长度发生变化时,函数内的len和cap会增长和扩容,而函数外的则不会
chan
channel是Golang在语言层面提供的goroutine间的通信方式,比Unix管道更易用也更轻便。channel主要用于进程内各goroutine间通信,如果需要跨进程通信,建议使用分布式系统的方法来解决。
接下来我们现看一下chan的基本数据结构,再来分析一下chan的底层原理
type hchan struct {
qcount uint // 当前队列中剩余元素个数
dataqsiz uint // 环形队列长度,即可以存放的元素个数
buf unsafe.Pointer // 环形队列指针
elemsize uint16 // 每个元素的大小
closed uint32 // 标识关闭状态
elemtype *_type // 元素类型
sendx uint // 队列下标,指示元素写入时存放到队列中的位置
recvx uint // 队列下标,指示元素从队列的该位置读出
sendq waitq
recvq waitq // 等待读消息的goroutine队列 lock mutex // 互斥锁,chan不允许并发读写 lock mutex // 互斥锁,chan不允许并发读写
lock mutex // 互斥锁,chan不允许并发读写
}
qcount 字段代表当前队列剩余元素的个数
dataqsiz 环形指针,当chan有缓冲区的时候,可以存放多少个元素
buf 环形指针,缓冲队列,如果有人往chan中写数据,没人读则会暂时写到缓冲队列中
sendx 环形队列中写队列的下标(比如一个程序往chan中要写数据,那么如果有缓冲区则根据这个下表来决定写入的下标地址)
recvx 环形队列中读队列的下标(比如一个程序从chan中读取数据,那么读取哪一个数据呢?可以根据这个下标来决定)
sendq 存放当前没有缓冲区还往队列中写入的goroutine,如果有人读则会唤醒该goroutine
recvq 存放当前chan数据为空(缓冲区没数据,sendq中也没数据)的goroutine,等待sendq中有数据后在释放recvq中的goroutine程序
map
老规矩,还是先来参考下map的数据结构
type hmap struct {
count int
// 代表哈希表中的元素个数,调用len(map)时,返回的就是该字段值。
flags uint8
// 状态标志(是否处于正在写入的状态等)
B uint8
// buckets(桶)的对数
// 如果B=5,则buckets数组的长度 = 2^B=32,意味着有32个桶
noverflow uint16
// 溢出桶的数量
hash0 uint32
// 生成hash的随机数种子
buckets unsafe.Pointer
// 指向buckets数组的指针,数组大小为2^B,如果元素个数为0,它为nil。
oldbuckets unsafe.Pointer
// 如果发生扩容,oldbuckets是指向老的buckets数组的指针,老的buckets数组大小是新的buckets的1/2;非扩容状态下,它为nil。
nevacuate uintptr
// 表示扩容进度,小于此地址的buckets代表已搬迁完成。
extra *mapextra
// 存储溢出桶,这个字段是为了优化GC扫描而设计的,下面详细介绍
}
type mapextra struct {
overflow *[]*bmap
// overflow 包含的是 hmap.buckets 的 overflow 的 buckets
oldoverflow *[]*bma
// oldoverflow 包含扩容时 hmap.oldbuckets 的 overflow 的 bucket
nextOverflow *bmap
// 指向空闲的 overflow bucket 的指针
}
每个map都会指向一个hmap指针大概8b,接下来我们着重介绍下buckets、oldbuckets、extra 这三个字段
count 代表当前map中的数量,一般用len()方法可以查询到
flags 代表当前状态,map不能被同时写入和删除,保证原子性
B 代表桶的对数 假如B=3 那么桶的对数为 8个
noverflow 溢出桶的数量
hash0 随机数
buckets 指向buckets数组的指针,数组的个数为2^B个
oldbuckets 库容时用于存放历史数据
nevacuate 扩容进度
extra 下面会着重介绍
我们先来介绍一下buckets这个字段
其底层存放多个buckets,数量就是2^B个
每个bucket的结构如下
type bmap struct {
tophash [8]uint8 //存储哈希值的高8位
data byte[1] //key value数据:key/key/key/.../value/value/value...
overflow *bmap //溢出bucket的地址
}
tophash 是个数组存放每个key的高8值,用于判断当前key是否在这个bucket中
data kkk vvv的方式存放map
overflow 这个字段我们先来介绍一下hash冲突,比如一个key 被hash放到了1个bucket中,如果bucket已经满了(超过了8个元素),那么则会通过链表的方式连接到下一个bmap结构
存储结构我们先说这么多,接下来我们聊一下map结构是如何扩容的?
说到扩容我们先了解一个公式
负载因子 = 键数量/bucket数量
比如说我们当前桶的对数为3 也就是支持有8个bucket,每个bucket能存放8个kv(8*8=64)
负载因子= 64/8 = 8
扩容方式1:存量空间不够扩容
当负载因子> 6.5的时候就会对这个map进行扩容
但是又不能直接copy过来对吧,所以扩容也是有策略的
比如说当前的负载因子是6.4 再插入一个新的元素就需要进行扩容了,那么此时的数据结构为
buckets -> buckets(原bucket为空)
-> buckets1(新申请的bucket也是新数据)
oldbuckets -> buckets(之前有数据的buckets)
这是新数据会插入到新申请的buckets1中,然后将oldbuckets中的数据回写到buckets中(每插入1条新数据,回写两条原数据)
扩容方式2:等量扩容
当如在因子没那么大,但是map由于增加 删除 修改等操作,造成了bucket中的overflow数据比较分散不集中
这时会重新排列一下overflow 增加效率
那么map是怎么查找和删除的呢?
查找过程如下:
根据key值算出哈希值
取哈希值低位与hmap.B取模确定bucket位置
取哈希值高位在tophash数组中查询
如果tophash[i]中存储值也哈希值相等,则去找到该bucket中的key值进行比较
当前bucket没有找到,则继续从下个overflow的bucket中查找。
如果当前处于搬迁过程,则优先从oldbuckets查找
注:如果查找不到,也不会返回空值,而是返回相应类型的0值。
插入过程
通过key计算出hash值
用hash值的低位值与hmap.B取模计算出becket位置
通过hash高8位在tophash中查找,找到更改,找不到则插入