这是我参与「第三届青训营 -后端场」笔记创作活动的第5篇笔记
string底层实现
string的数据结构如下
- golang不用特定标识符表示字符串的结尾
- golang采用string结构体来共同描述一个字符串
- data是指向字符串开始位置的指针
- len表示字符串的字节个数(非字符个数),因为golang采用utf8的变长编码方式,故不能采用字符个数
string的底层细节
- golang将string类型分配到只读内存段,因此不能通过下标的方式修改
- 多个string变量可以公用同一个字符串的某个部分
- 可转为字节切片对字符串内容进行修改,不过改动的是拷贝后的字符串
slice的扩容机制
旧版本的扩容机制
- 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)
- 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap)
- 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的 1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
- 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)
再谈谈扩容之后的数组一定是新的么?这个不一定,分两种情况。
- 情况一:切片的cap还够用,则扩容后还是原来的底层数组
- 情况二:切片的cap不够用,则扩容后会开辟新的数组,将原来的值拷贝后再扩容,此时的底层数组是新开辟的而不是原来的
新的扩容机制
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
const threshold = 256
if old.cap < threshold {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
// Transition from growing 2x for small slices
// to growing 1.25x for large slices. This formula
// gives a smooth-ish transition between the two.
newcap += (newcap + 3*threshold) / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
注意:如果用 range 的方式去遍历一个切片,拿到的 Value 其实是切片里面的值拷贝,并非引用传递,所以直接改 Value 是达不到更改原切片值的目的的,需要通过 &slice[index] 获取真实的地址。
golang的map
map类型的变量本质上是一个指针,指向hmap结构体
- golang选择桶采用与运算,2^B = 桶的个数,B是幂次
- golang对桶扩容时采用渐进式扩容,即旧新桶数据迁移分摊到多次的哈希表操作中,而不是一次性迁移,从而避免性能瞬时抖动
桶的实现——bmap结构体
- 前8个字节是每个键值对哈希值的前8位tophash
- 每个桶可存储8对键值对,因此随后是8个键,再是8个值,根据不同的类型占用不同的大小
- overflow指向溢出桶
桶溢出机制
- golang认为桶的个数大于2^4此方,即hamp的B>4时,使用溢出桶的概率较大, 因此默认分配2^(B-4)个溢出桶
- 常规桶和溢出桶在内存中是连续分布的
- hmap的extra指向描述溢出桶的结构体mapextra
- noverflow表示已用溢出桶个数
- mapextra结构体中的overflow是一个slice
- 图中的第2号桶假设已用满,它的bmap结构体的overflow就会指向32号位置的下个空闲溢出桶
扩容机制
翻倍扩容
等量扩容
- 当存在大量键值对被删除时,桶中的键值对存储变得稀疏,需要使用等量扩容
- 使得键值对排列的更加紧凑
sync.Map简要介绍
golang中普通的map并非是并发安全的,对于高并发场景的使用,需要加锁。另外golang也提供了并发安全的map,正是在sync包下的Map。下面简要介绍一下sync.Map~
sync.Map的原理介绍:sync.Map里头有两个map一个是专门用于读的read map,另一个才是提供读写的dirty map;优先读read map,若不存在则加锁穿透读dirty map,同时记录一个未从read map读到的计数,当计数到达一定值,就将read map用dirty map进行覆盖。
优点:是官方出的,是亲儿子;通过空间换时间的方式;读写分离; 缺点:不适用于大量写的场景,这样会导致read map读不到数据而进一步加锁读取,同时dirty map也会一直晋升为read map,整体性能较差。
适用场景:大量读,少量写
参考链接
幼麟实验室bilibili:space.bilibili.com/567195437