Go中“0字节”
- int
sizeof 查看字节数
int 8字节 64位;4字节 32位(根据系统)
int32永远4字节
指针也是跟随系统字长,其底层其实是无符号的64位整型
- 空结构体
空结构体有地址没内存
空结构体独立的地址都为 zerobase(指针)
%p 打印地址
主要为了节约内存
map【string】struct{}{},相当于hashset 只有键没有值
chan的负载可以为空结构体,当发信号使用
nil不能是空结构体
数组字符串切片
- 字符串
底层为结构体 ,runtime包中unsafe.Pointer 指向byte数组
unicode 统一的字符集,至少3 个字节表示,utf8变长格式
访问字符串"你好"fori得到每个字节,forr range自动解码为rune
rune就是utf8
- 切片
字面量创建 []int{} 编译时确定
运行时创建 make() 直接调用runtime的makeSlice
因为数组是连续的空间,继续用原来的数组可能到达其他变量的空间,所以切片扩容新开数组
切片扩容时,并发不安全,注意加锁(第一个协程追加扩容老切片废除,第二个协程读取老切片的值就会出问题)
map
redis的本质就是一个大的hashmap
a:A 举例:
先对key(a)哈希取位置
开放寻址法一直找到不为空的值插入
取的话也是一样,a哈希找位置,不为a则往后找
拉链法横向的对应槽(Slot),每个槽对用的kv为桶
- hmap,bmap
map在go中底层为: hmap,其中:
count kv个数
hash0 代表哈希种子,哈希算法用到
buckets 中有 2的B次幂个 桶(bmap)
bmap
tophash 哈希数组(哈希值的高8位) —> 为了快速访问
keys elems kv数组
overflow 指针,如果这个bmap 超过 8 个还要存,那么指向 新的 bmap
- 初始化
调用make初始化
先确定B,先创建正常的bmap,同时会多创建几个溢出桶,
hmap 底层有 NextOverflow 指向下一个可用的溢出桶,bmap中的overflow指针溢出后会指向该桶
字面量初始化
掉用make,然后赋值
- 访问:
hash0 + key 确认桶号
通过B 取二进制后几位确认桶
然后找tophash ,key 和 溢出桶的hash
没找到说明key 不存在,写入同样操作,没有的话插入一个
map扩容
哈希碰撞严重,map桶溢出太多,趋向链表
太多溢出桶(溢出桶>普通桶)
整理(等量)扩容 – >溢出桶太多(存在之前的数据被删掉导致)
翻倍扩容(数据太多)
创建一组新桶
oldbuckets 指向原有的桶数组,buckets指向新的桶数组
map标记(flags)为扩容状态
渐进式驱逐
每次操作一个旧桶,将旧桶数据驱逐到新桶(根据B操作进行划分)
所有旧桶驱逐完毕,oldbuckets 回收
map并发操作
A协程读取时桶不会驱逐,B协程写入驱逐这个桶
map加锁会影响性能
Map
- 总结构:
Mutex
read 下面有个万能map
dirty 下面有个map
misses 没有命中
将不会扩容的操作和扩容的操作分离
正常的话走read ,找到的话修改即可
找不到的话,上锁,操作dirtymap,所以每次只有一个协程操作dirtymap
然后read中的amended 为true,表示有追加,说明read中map数据不准确,修改和读走read就行
只有追加元素可能扩容时才走dirtymap
如果在dirtymap中读到键值,则misses+1
misses = len(dirtmap) ,dirty提升变为read中map
追加false ,misses=0,dirtymap = nil(走dirty时在变为和read中一样)
删除更麻烦
正常删除,追加为false,将k后的指针置为nil,指不到v了,过段时间GC就会将v干掉
追加后的删除,指针为nil
上提后dirtymap重建时,指针为expunded(删除)
访问read时直接将其干掉就行,因为dirty中没有,不用考虑一致的问题
接口
结构体实现接口,底层会实现值和指针的方法
而指针实现接口,底层只实现指针的方法
正常接口 iface,空接口 eface 除了指针,第一个字段比较简单(类型)
其实是用eface承载传入,最大用途任意类型入参
内存对齐
CPU操作数据按照字长来操作
跨字长变量影响原子操作,并且影响性能
基本类型根据对齐系数判断放置位置
结构体偏移量可通过调整成员顺序节省空间,最后分情况补长
根据结构体的对其系数判断第一个位置的地址
空结构体独立使用地址默认,包在其他结构体中则不一定