go语言底层机制与实现的探讨 | 青训营笔记

258 阅读3分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第5篇笔记

string底层实现

string的数据结构如下

image.png

  • golang不用特定标识符表示字符串的结尾
  • golang采用string结构体来共同描述一个字符串
  • data是指向字符串开始位置的指针
  • len表示字符串的字节个数(非字符个数),因为golang采用utf8的变长编码方式,故不能采用字符个数

string的底层细节

image.png

  • golang将string类型分配到只读内存段,因此不能通过下标的方式修改
  • 多个string变量可以公用同一个字符串的某个部分
  • 可转为字节切片对字符串内容进行修改,不过改动的是拷贝后的字符串

slice的扩容机制

旧版本的扩容机制

image.png

  • 首先判断,如果新申请容量(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结构体

image.png

  • golang选择桶采用与运算,2^B = 桶的个数,B是幂次
  • golang对桶扩容时采用渐进式扩容,即旧新桶数据迁移分摊到多次的哈希表操作中,而不是一次性迁移,从而避免性能瞬时抖动

桶的实现——bmap结构体

image.png

  • 前8个字节是每个键值对哈希值的前8位tophash
  • 每个桶可存储8对键值对,因此随后是8个键,再是8个值,根据不同的类型占用不同的大小
  • overflow指向溢出桶

桶溢出机制

image.png

  • golang认为桶的个数大于2^4此方,即hamp的B>4时,使用溢出桶的概率较大, 因此默认分配2^(B-4)个溢出桶
  • 常规桶和溢出桶在内存中是连续分布的
  • hmap的extra指向描述溢出桶的结构体mapextra
  • noverflow表示已用溢出桶个数
  • mapextra结构体中的overflow是一个slice
  • 图中的第2号桶假设已用满,它的bmap结构体的overflow就会指向32号位置的下个空闲溢出桶

扩容机制

image.png

翻倍扩容

image.png

等量扩容

image.png

  • 当存在大量键值对被删除时,桶中的键值对存储变得稀疏,需要使用等量扩容
  • 使得键值对排列的更加紧凑

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