【Go笔记】数组与切片
数组的特性
- 声明时需要确定类型和长度,可以不指定长度,在编译时推断
- 声明的同时可以赋值
- Go中数组在赋值和函数调用时的形参都是值复制,是两个完全不同的数组
- 数组在复制之后是两个不同的数组,涉及到内存的复制操作,可能需要考虑性能开销,是深复制
数组的底层原理
- 数组在编译时就要确认长度和类型
- AST阶段的数据类型为
TARRAY
,内部的Array
struct存储了数组的类型及大小,新数组的Op操作是OARRAYLIT
- 数组字面量(源代码相关部分)初始化在类型检查阶段进行,声明赋值工作也在这个阶段完成,并判断是不是语法糖
- 在函数walk遍历阶段,数组长度小于4的时候,运行时会被放在栈中,大于4时会被放在内存的静态只读区
- 在类型检查阶段会判断一部分数组越界情况,在索引为非整数,复数,越界时报错
- 未命名常量和命名常量作为索引的时候在编译时可以通过优化检测出越界,并在运行时报错。可以通过编译,但是代码会直接优化成崩溃SSA,直接崩溃而不进入到实际逻辑。
types/type.go
切片的特性
- 与数组类似,但是长度可变
- 用
len(slice)
获取切片长度,用cap(slice)
获取切片容量 - 切片的容量默认与长度相同,但是也可以自定义切片的容量
- 切片中的数据依然是内存中的一片连续区域
- 在只声明不赋值的情况下,切片的值预设为
nil
- 在截取切片的时候,是
[)
,顾头不顾腚 - 被截取的切片依然指向原切片,指向同一块内存,不是新的切片,因此更改原切片的时候可能会影响到截取下来的切片
- 切片的复制也是值复制,但是只复制
SliceHeader
结构,即指向的仍然是同一块内存,因此复制成本很小,是浅复制 - 可以使用
copy(dst, src)
来对切片做深复制,可以在切片之间复制,也可以在切片和数组之间复制 - 可以使用
append()
进行扩容,使用“自截”进行缩容。二者同时使用进行剔除中间元素,并且不会申请额外的内存空间,效率很高
切片底层原理
- 当使用
[]int{1, 2, 3}
来创建切片的时候,会创建一个array数组[3]int{1, 2, 3}
并存在静态区中,同时在堆区中创建一个新的切片,程序启动时将静态区的数据复制到堆区,以加快切片初始化过程 - 当使用
make([]int, 3, 4)
来创建切片的时候,在类型检查阶段中,节点的Op为OMAKESLICE
,左节点存储长度3,右节点存储容量4 - 使用
make
初始化的时候,会有字面量内存逃逸问题。若大小小于maxImplicitStackVarSize
,默认为64KB,会直接在栈上分配,否则会逃逸到堆中 - 使用
mallocgc()
在堆中申请内存
¶扩容
- 新申请容量大于两倍的旧容量 - 最终容量是新申请的容量
- 旧切片的长度小于1024 - 最终容量是旧容量的两倍
- 旧切片的长度大于等于1024 - 最终容量从旧容量开始循环增加原来的1/4,直到最终容量大于等于申请容量
- 最终容量超过了int的最大值 - 最终容量就是新申请容量
- 扩容后新的切片不一定有新的地址,因此需要自我赋值来保证安全
¶截取
- 截取就是将同一数据源的不同位置的地址分出来就好了
总结
- 切片需要维护底层元素地址,还要维护长度和容量
- 数组的复制是深复制,切片的复制默认是浅复制,但可以调用
copy
函数来进行深复制 - 切片字面量初始化会以数组的形式存储在静态区中,在运行时复制值来初始化
- 使用
make
函数如果小于64KB,在栈中初始化;若大于64KB,则在堆中初始化 make
初始化涉及内存分配行为,是在运行时来操作
【Go笔记】数组与切片
https://study.0x535a.cn/go-note/go-array-slice/