写在开头
非原创,知识搬运工,本节介绍了数组、切片、map
带着问题去阅读
- 罗列切片的创建方式
- 切片和数组的区别
- 切片的底层结构长什么样
- 对于切片s,s[i]实际改变的是什么
- 扩容的机制
- new和make的区别
- 原生的map存在并发问题吗
- 了解map的底层结构
1.数组
An array is a numbered sequence of elements of a single type, called the element type. The number of elements is called the length of the array and is never negative.
根据ref描述可知数组是单一元素的集合
var arr =[...]int{1,2,3}
var arr1 [3]
数组的比较也没啥好说的,元素类型和长度一样就可以用等价==对比(最经典的就是传参)
查看底层实现demo.go
var a= [...]int{0,1,2,3,4,5,6,7,8,9}
fmt.Println(a)
启动dlv
dlv debug main.go
b main.go:9 //var a的行数
c
disass //查看底层汇编
不像string在runtime上有特殊的底层结构,实质上就是分配一块连续的内存,那么数组的大小就和类型和长度有关
知识点复习
string的len会读取底层结构体的len字段,和获取数组的长度一样都是直接从内存的某一块中去获取数据数组的长度是在声明时就是固定的,它在内存中的表现是一块连续的内存,并且GO语言没有提供删除数组元素的方法。此时如果我们有一个需求要实现一个变长数组呢?那么我们就需要解决以下问题:
- 要像定长数组一样,变长数组也要支持索引和随机访问;
- 定义的时候到底要分配多长的连续空间?
- 空间不够用了怎么办?
- 空间剩余很多如何回收?
这就引出了我们的下文主角--切片slice
2.slice
切片和数组的声明区别在于是否字面量声明长度
array:
var arr [3]int
slice:
var sli []int
A slice is a descriptor for a contiguous segment of an underlying array and provides access to a numbered sequence of elements from that array. A slice type denotes the set of all slices of arrays of its element type. The number of elements is called the length of the slice and is never negative. The value of an uninitialized slice is nil.
切片是底层数组连续段的合集,长度是元素的数量,切片一旦被初始化,总是与其元素的底层数组相关联
也就是说切片是数组的“一段”,切片和衍生出切片的数组共享存储空间
var a =[...]int{0,1,2,3,4,5,6,7,8,9}
var b = a[:7]
b[0]=1
fmt.Println(a)
b[7] =2 //报错,超出范围
fmt.Println(a)
看不太懂汇编,但与上图看起来很相似,猜到大概就算先建立数组再上下移动指针划分切片区间范围
2.1make
另一种切片声明方式是利用make
/*
Type:数据类型,必要参数,Type 的值只能是 slice、 map、 channel 这三种数据类型
len:数据类型实际占用的内存空间长度,map、 channel 是可选参数,slice 是必要参数。
cap:为数据类型提前预留的内存空间长度,可选参数。所谓的提前预留是当前为数据类型申请内存空间的时候,提前申请好额外的内存空间,这样可以避免二次分配内存带来的开销,大大提高程序的性能。
*/
func make(t Type, size ...IntegerType) Type
var a = make([]int,3)
我们看到make底层是使用了makeslice方法,具体方法自己看吧
r
b makeslice
c //找到文件是在runtime.slice.go
makeslice主要完成以下工作:
- 创建slice前要判断是否超过分配的最大内存(溢出检查)
- 再调用mallocgc在堆上申请一片连续的内存
func makeslice(et *_type, len, cap int) unsafe.Pointer {
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
if overflow || mem > maxAlloc || len < 0 || len > cap {
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
return mallocgc(mem, et, true)
}
小结一下make就是为需要的类型(slice,chan,map)分配并初始化所需的内存空间和结构,返回的是类型本身(make函数层面,makeslice返回的是地址/指针)
书接上文,既然没有关联数组,makeslice直接当老大分配并初始化类型所需的内存和结构,并返回复合类型本身(slice是复合类型)的地址
makeslice返回地址后,第8行的编译代码并没有结束,而是划分了地址空间
我们在makeslice所在的文件slice.go中找到runtime对slice的定义
type slice struct {
array unsafe.Pointer
len int
cap int
}
这个时候我们不就恍然大悟了,知道划分这三个地址的原因了,slice在运行时是一个有三个成员的结构体,那么len和cap就能直接找到,时间复杂度为O(1),以及上面三行的汇编就是对这三个值赋值的过程
举一反三,如果不是利用make声明,而是字面量声明,那么slice结构体中的array成员就应该是衍生出切片的数组(或者切片的切片)的地址,也就是该切片它妈妈的家庭门牌号。这个不就是典型的装饰器模式?所以我们知道了,切片是利用装饰器模型解决了寻址问题和如何分配内存问题(用一个结构体内存表示)。
最后补充一下官方ref对make的描述
Call Core type Result
make(T, n) slice slice of type T with length n and capacity n
make(T, n, m) slice slice of type T with length n and capacity m
make(T) map map of type T
make(T, n) map map of type T with initial space for approximately n elements
make(T) channel unbuffered channel of type T
make(T, n) channel buffered channel of type T, buffer size n
make适用于三种类型,我们根据实验可以猜到不同的类型调用底层不同的函数
2.2new
不常用做补充
官网还有种产生slice的方法就是new
new([10]int)[0:5] //先创建长度为10的空数组,再直接切5个
底层调用newobect,直接是一个分配内存函数
func newobject(typ *_type) unsafe.Pointer {
return mallocgc(typ.size, typ, true)
}
我们修改下代码对比下
new([10]int)
打印一下发现打印的是一个内存地址,确实没毛病啊,mallocgc是返回一个地址指针,我们后续需要自己用切片的形式[:5]来初始化这个切片!而make是在mallocgc基础上帮我们多包装了一步切片的初始化
官方文档对new的描述
// The new built-in function allocates memory. The first argument is a type,
// not a value, and the value returned is a pointer to a newly
// allocated zero value of that type.
func new(Type) *Type
他是一个分配内存的内置函数,返回该类型新分配的零指针(对每一个bit都置为0)
2.2.1 new和make区别
从方法签名上我们就可以看出区别:
- new是返回类型指针,make是返回类型本身
- make只支持三种类型,其他类型不可以使用,初始化类型本身--即初始化零值
实际上在日常开发中new是使用比较少的,可以被替代的。
2.3扩容
空间不够如何扩容?
那如何让这个水桶装更多的水?,从下面这个例子说明
这是给a切片追加数据
a=append(a,11)
根据汇编找到底层实现函数是slice.go/growslice,贴一下伪代码(go1.18)
func growslice(et *_type, old slice, cap int) slice {
#一些检查
//先尝试两倍扩容
//如果需要的容量超过原来切片的两倍,直接使用需要的容量作为新容量,否则,当原切片小于256,直接翻倍,大于256就不停增加(需要的容量+3*256)/4直到容量满足(好多资料都是基于1024,但我这1.18源代码是256)
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
}
}
}
#计算内存所需代码
#内存分配代码
//这个结构体上文见过
return slice{p, old.len, newcap}
小结一下扩容算法:如果需要的容量超过原来切片的两倍,直接使用需要的容量作为新容量,否则,当原切片小于256,直接翻倍,大于256就不停增加(需要的容量+3*256)/4直到容量满足
注意扩容时对slice的array成员也进行了重新赋值,他是动态分配新数组,新数组长度按一定规律扩容(上面算法),把旧数组拷贝到新数组,之后新数组成了底层数组,旧数组被垃圾回收(拷贝到新空间)
func main(){
var a =[...]int{1}
var b=a[:]
b=append(b,2)
fmt.Println(a,b)
}
// [1] [1,2]
一旦扩容,切片和原数组就会解除绑定,后续修改不会反应到原数组上,这从上文growslice最后返回新切片也能看出
a := [1]int{1}
s1 := a[:]
fmt.Println(cap(s1)) //1
s1 = append(s1, 2)
fmt.Println(cap(s1)) //2
s1[0] = 2
fmt.Println(a) //[1]
2.4传slice和slice指针的区别
从上面学习中我们可以获知slice实际是一个结构体,我们现在讨论当slice作为函数参数传递的情况:
- 作为值传递
- 作为指针传递
很好理解:若直接传 slice,在调用者看来,实参 slice 并不会被函数中的操作改变;若传的是 slice 的指针,在调用者看来,是会被改变原 slice 的。
但是我们上文说过slice是能通过下标访问的方式来直接操作底层数组的,因此会出现这种情况
func TestPaseValue(t *testing.T) {
p := func(s []int) {
for i := range s {
s[i]++
}
}
s := []int{1, 1, 1}
p(s)
t.Log(s)
}
222
即使是值传递了一个被拷贝的副本,仍改变了原始slice的底层数组,很神奇是吧,但是外层的slice却没有改变(指slice结构体三个成员的值,他们包裹在底层数组外面),若想改变外层,可以返回一个slice或使用指针
最后空间回收问题,实际上就是利用字面量声明诞生一个新切片,旧的就交给系统自动回收就好
a := b[:2]
至此变长数组slice讲解完毕
小结一下切片和数组区别
3.map
map在声明后要用make进行初始化,若不初始化则为nil,无法执行后续插入操作
var m2 map[int]int
//m2==nil true
// m2[2]=2 panic: assignment to entry in nil map 并没有分配地址
m3 := map[int]int {} // m3==nil false
3.1map的大小
func main(){
var a map[string]int
fmt.Println(a,unsafe.Sizeof(a))
}
// [] 8
我们看到大小输出是8,哪些数据结构大小是8?int ?指针?底层跟这两八九不离十,dlv看一看很遗憾就是一块内存地址段mov qword ptr [rsp+0x18], 0x0,那我们根据前面的经验,string.go中有string结构体,slice.go有slice结构体,那map呢,看了文件我们发现有一个hmap结构体,是map的头部类型结构
// A header for a Go map.
type hmap struct {
count int // # live cells == size of map. Must be first (used by len() builtin)
flags uint8
B uint8 // 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
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 // optional fields
}
有经验的同学一眼就看出来了,这不就跟哈希链表很像吗(redis不就这样存键值对吗,利用哈希存键,哈希冲突时,拉出一个哈希链表/哈希桶),挑几个重要的解释
- count是当前map中元素的个数(len会直接找这个)
- B。B=log2(bucket) 或者说B^2 =buckets
- overflow是溢出桶的最大数量
- hash0哈希种子
- buckets指向哈希桶的指针
- oldbuckets是在map扩容前指向的前一个buckets指针
- nevacuate是扩容进度计数器
- extra与overflow有关
1右移三位1000b = 8d,哈希桶填满了,且map没到扩容条件时会建立overflow,overflow挂在bucket末尾即overflow头指针指向bucket末尾指针
bucket长啥样
// A bucket for a Go map.
type bmap struct {
// tophash generally contains the top byte of the hash value
// for each key in this bucket. If tophash[0] < minTopHash,
// tophash[0] is a bucket evacuation state instead.
tophash [bucketCnt]uint8
// Followed by bucketCnt keys and then bucketCnt elems.
// NOTE: packing all the keys together and then all the elems together makes the
// code a bit more complicated than alternating key/elem/key/elem/... but it allows
// us to eliminate padding which would be needed for, e.g., map[int64]int8.
// Followed by an overflow pointer.
}
就一个长度为8的数组字段,不再多了,当map插入数据,先用哈希函数对key做运算获得hashcode,低位用于选定bucket,高位用于在bmp中确定key存的值的位置
map扩容再看看?
确实也没有cap,传入的第三个值是hmp
其实map会对底层内存自动管理,当插入元素超出一定个数后自动扩容,在mapassign函数中有这几行代码
if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
hashGrow(t, h)
goto again // Growing the table invalidates everything, so try again
}
overLoadFactor会检查负载因子LoadFactor,当count>loadfactor*2^B或over flow bucket溢出桶太多时自动扩容,again是该函数中的一段标志,也就是说如果中途发现还不够还要继续扩容(建立一个更大规模的数组bmp,但是真正把数组的旧数据踢到新桶中的时候是操作该数据的时候,oldbucket指向旧桶,直到数据迁移完再释放旧桶)。
3.2map的并发问题
package main
func main() {
m := make(map[int]int)
go func() {
for {
_ = m[1]
}
}()
go func() {
for {
m[2] = 2
}
}()
select {}
}
冲突检测(吐槽下我这服务器1CPU1核只有一个本地队列,运行根本不出现数据竞争)
go run -race main.go
并发读写时是有冲突的,它本身并没有实现锁,所以我们可以自定义带锁的map结构体,或使用sync.Map(自带成员 mutex锁)
参考
1.append
2.ref
3.深度解密slice
4.深入了解slice
5.一文解决map并发问题
6.gomap扩容机制