go的数据类型深入解析
2023-02-18 00:16:50
技术
int bool float string array slice map chan
主要是 map 和 chan
int
int(有符号)和uint(无符号)的大小与操作系统相关,在32位操作系统中大小为32位(4字节),64位操作系统中大小为64位(8字节)
int 8 字节
int8 1 字节
int16 2 字节
int32 4 字节
int64 8 字节
它们是不同的类型(type)
float
float32,即单精度,32位(4字节),其中1位用来符号,8位用来指数,剩下的23位表示尾数,精确到小数点后7位 float64,即双精度,64位(8字节),其中1位用来符号,11位用来指数,剩下的52位表示尾数,精确到小数点后15位
bool
只有2种:true和false bool值并不会隐式转换为数字值0或1,反之亦然。必须使用一个显式的if语句辅助转换:
i := 0
if b {
i = 1
}
string
字符串可以包含任意的数据,包括byte值0,但是通常是用来包含人类可读的文本。文本字符串通常被解释为采用UTF8编码的Unicode码点(rune)序列。 一个字符串是一个不可改变的字节序列,实际上就是占用了一片连续的内存空间,所以能理解为一个字符数组 在go的源码中可以找到string的数据结构
type stringStruct struct {
array unsafe.Pointer // 指向一个 [length]byte 的数组
length int // 长度
}
其成员有2个,一个指向数组的指针,一个是所指数组的长度 示例图:go语言圣经3.5.1
array
cmd/compile/internal/types2.Array go 源码中数组的结构
type Array struct {
len int64 // 数组长度,即元素个数
elem Type // 元素类型
}
数组占用一片连续的内存空间
slice
src/runtime/slice.go go 源码中slice的结构
type slice struct {
array unsafe.Pointer // 指向数组的指针
len int // 切片长度
cap int // 切片容量
}
其成员有3个,一个指向数组的指针,一个是切片的长度,还有一个是切片的容量
切片的长度就是切片所包含的元素个数。
切片的容量是从它的第一个元素开始数,到其底层数组
元素末尾的个数。
map
go 源码中map的结构 src/runtime/map.go
type hmap struct {
count int // map当前元素个数
flags uint8 // 读、写、扩容、迭代等标记,用于记录map当前状态
B uint8 // 用于计算桶大小, bucketSize = 1 << B ;log_2 of # of buckets (can hold up to loadFactor * 2^B items)
noverflow uint16 // 溢出桶个数,当溢出桶个数过多时,这个值是一个近似值 approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed,计算key哈希值的随机值,保证一个key在不同map中存放的位置是随机的
buckets unsafe.Pointer // 当前哈希桶首地址 array of 2^B Buckets. may be nil if count==0.
oldbuckets unsafe.Pointer // 旧哈希桶首地址 previous bucket array of half the size, non-nil only when growing
nevacuate uintptr // 已迁移哈希桶个数 progress counter for evacuation (buckets less than this have been evacuated)
extra *mapextra // 扩展字段,不一定每个map都需要 optional fields
}
map是使用hash表(哈希表/散列表)来实现的,一个hash表里可以有多个hash节点,也叫bucket(桶/哈希桶),而每个bucket就保存了map中的一个或一组键值对。
https://blog.csdn.net/fengshenyun/article/details/100582679
bucket的数据结构
type bmap struct {
tophash [8]uint8 //存储哈希值的高8位,哈希值相同的键(准确的说是哈希值低位相同的键)存入当前bucket时会将哈希值的高位存储在该数组中,以方便后续匹配。
data byte[1] //key value数据:key/key/key/.../value/value/value...,如此存放是为了节省字节对齐带来的空间浪费
overflow *bmap //溢出bucket的地址,指向的是下一个bucket,据此将所有冲突的键连接起来。
}
https://www.cnblogs.com/failymao/p/14902607.html
https://www.cnblogs.com/dawnlight/p/15552513.html
map数据结构最终梳理
- hmap是数组(切片),组成元素是bucket(bmap,以便区分,这里后面都用bucket替代bmap);bucket是链表,指针指向下一个扩充的bucket。
- map存储的是key/value对(k/v对、键值对)
- 当key传进来时,会经过一个hash函数运算得到一个唯一的hash值,在go中会将这个hash值分为高位和低位
- 如hash值:1314131425002500,高8位为13141314,低8位为25002500
- 低位用于寻找key属于hmap的哪个bucket,高位用于寻找bucket中的哪个key
- 如果key计算出的hash值低位相同,那么这些key存入相同的bucket中,在bucket中会存储它们的高位,用于存入的key的快速预览
- bucket是以key/key/key/.../value/value/value...的方式存储k/v对的,这是为了节省字节对齐带来的空间浪费
- 每个bucket只能存储8个k/v对,超过8个则会扩充一个新的bucket存放溢出的k/v对,然后指针指向新的bucket,以此类推,形成一个链表
负载因子与渐进式扩容
负载因子 = key数量/bucket数量 (len(map)/2^B)
- 因子过小,说明空间利用率低:key少bucket多,说明map的bucket数组个数多,一个bucket中存放的key少
- 因子过大,说明冲突严重,存取效率低:key多bucket少,说明map的bucket数组个数少,一个bucket中存放的key多,溢出bucket严重
每个哈希表的实现对负载因子容忍程度不同,go的阈值为6.5,即平均每个bucket存放6.5个k/v对;而redis的阈值为1,因为redis里每个bucket只存放一个k/v对
扩容条件
- 1.负载因子 > 6.5时,也即平均每个bucket存储的键值对达到6.5个。
- 2.overflow(溢出bucket)数量 > 2^15时,也即overflow数量超过32768时。
增量扩容
因子过大,则会新建一个bucket数组将hmap,大小为原有bucket数组的两倍,并将原有bucket数组的数据渐进式地迁移到新bucket数组,即每次访问map时都会触发一次搬迁,每次搬迁2个键值对。
hmap的buckets
oldbuckets
nevacuate
字段就是用来干这个的
等量扩容
buckets数量不变,只是重新做一遍类似增量扩容的搬迁动作,把松散的键值对重新排列一次,以使bucket的使用率更高,进而保证更快的存取。例如过于频繁的对k/v进行增删操作,而这些k/v集中在某个bucket里,则会导致溢出bucket严重,影响存取效率
插入与查找
插入
- 根据key值算出哈希值
- 取哈希值低位与hmap.B取模确定bucket位置
- 查找该key是否已经存在,如果存在则直接更新值
- 如果没找到将key,将key插入
查找
- 根据key值算出哈希值
- 取哈希值低位与hmap.B取模确定bucket位置
- 取哈希值高位在tophash数组中查询
- 如果tophashi中存储值也哈希值相等,则去找到该bucket中的key值进行比较
- 当前bucket没有找到,则继续从下个overflow的bucket中查找。
- 如果当前处于搬迁过程,则优先从oldbuckets查找 注:如果查找不到,也不会返回空值,而是返回相应类型的0值。
chan
src/runtime/chan.go
type hchan struct {
qcount uint // 当前队列中剩余元素个数 total data in the queue
dataqsiz uint // 环形队列长度,即可以存放的元素个数 size of the circular queue
buf unsafe.Pointer // 环形队列指针 points to an array of dataqsiz elements
elemsize uint16 // 每个元素的大小
closed uint32 // 标识关闭状态
elemtype *_type // 元素类型 element type
sendx uint // 队列下标,指示元素写入时存放到队列中的位置 send index
recvx uint //队列下标,指示元素从队列的该位置读出 receive index
recvq waitq //等待读消息的goroutine队列 list of recv waiters
sendq waitq // 等待写消息的goroutine队列 list of send waiters
lock mutex //互斥锁,chan不允许并发读写
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
}
chan最核心的部分由一个环形队列和2个waitq组成,环形队列用于存放数据(带缓冲的情况下),waitq用于实现阻塞和恢复goroutine。
type waitq struct {
first *sudog
last *sudog
}
type sudog struct {
g *g
next *sudog
prev *sudog
elem unsafe.Pointer // data element (may point to stack)
acquiretime int64
releasetime int64
ticket uint32
// isSelect indicates g is participating in a select, so
// g.selectDone must be CAS'd to win the wake-up race.
isSelect bool
// success indicates whether communication over channel c
// succeeded. It is true if the goroutine was awoken because a
// value was delivered over channel c, and false if awoken
// because c was closed.
success bool
parent *sudog // semaRoot binary tree
waitlink *sudog // g.waiting list or semaRoot
waittail *sudog // semaRoot
c *hchan // channel
}
https://www.cnblogs.com/failymao/p/14891813.htmlhttps://i6448038.github.io/2019/04/11/go-channel/https://cloud.tencent.com/developer/article/1750350
最终梳理
channel在源码是hchan结构体,核心为1个循环队列、以及2个 分别等待读和写的 goroutine 等待队列(双向链表)
循环队列
qcount uint // 当前队列中剩余元素
dataqsiz uint // 队列长度,即可以存放的元素个数
buf unsafe.Pointer // 队列
sendx uint // 队列下标,指示元素写入时存放到队列中的位置
recvx uint // 队列下标,指示元素从队列的该位置读出
lock mutex // 互斥锁
在创建channel时,在内存中实例化了一个hchan的结构体,并返回一个ch指针。channel 是一个指针。
ch := make(chan int, 3)
在讨论之前先统一一下概念:
对于发送给channel的消息,channel是接收者,对channel来说是写操作;对于channel发送出去的消息,channel是发送者,对channel来说是读操作
sendx: 等待发送。需要发送给channel的元素的索引位置(index),即元素写入时存放到队列中的位置。
recvx: 等待接收。需要从channel接收的元素的索引位置(index),即从队列的该位置读出元素。
给channel发(send)数据 (ch<-xx)
// 1.创建一个channel
// 在内存中实例化了一个hchan的结构体,并返回一个ch指针。
// ch是指针,ch用使用队列 buf 来缓存数据
// 队列长度为dataqsiz为3,当前元素 qcount 为 0
// sendx和recvx均为 0
ch := make(chan int, 3)
// 2.将 一个元素写入 buf,
// 首先先加锁
// 将元素拷贝,传递给channel
// 索引 0 位置写入了新的元素,sendx变为1,即下一个元素需要写入索引1的位置;recvx不变,即下一个读操作从索引0的位置读取
// 队列长度为dataqsiz为3,当前元素 qcount 为 1
// 释放锁
ch<-1
ch<-1
ch<-1
// 队列缓存已满,再次写入会发生堵塞,这点我们后面再讲
// 每次写入sendx+1,写入了3个元素,因为是循环队列 sendx又变为0;因为没有读取,recvx一直不变还是0
// 3.从buf读取一个元素,和写元素类似
// 首先先加锁
// channel将元素传递给接收者
// 从索引 0 位置读出元素,recvx变为1,即需要读取的下一个元素位置为索引1;sendx为0,即下一个元素需要写入索引0的位置
// 队列长度为dataqsiz为3,当前元素 qcount 为 2
// 释放锁
<-ch
<-ch
<-ch
这个应该很有用,但太长了不想看 https://blog.csdn.net/kesenzhang/article/details/104488727
双向链表
主要实现了堵塞
recvq waitq //等待读消息的goroutine队列
sendq waitq // 等待写消息的goroutine队列
这部分与GPM调度有关,等我写好内存模型和调度后再继续写,to be continent ...