数组是什么?
go的数组是一种相同类型元素的集合,内存分配为一块连续的内存,数组的组成分为数组类型和数组长度,注意:go的数组长度是数组的一部分,对比数组是否相同,也要比对数组长度。
数组声明的方式?
切片是什么
切片可以理解为动态数组,声明时只需要指定类型即可。
数据结构
type SliceHeader struct {
Data uintptr // Data 是指向数组的指针;
Len int //Len 是当前切片的长度;
Cap int //Cap 是当前切片的容量,即 Data 数组的大小:
}
切片理解成一片连续的内存空间加上长度与容量的标识。
切片在运行时才会确定内容结构,所以操作需要依赖go语言的运行时。
切片初始化
1. 直接声明
var slice []int
创建出来的是 nil slice ,长度和容量都是0.和nil比较结果为true。
容易混淆的是 empty slice,它的长度和容量也都是0,所有的空切片都指向同一个地址,空切片创建方式
slice := make([]int,0)//或者
slice := []int{}
内部结构图如下
创建方式 | nil切片 | 空切片 |
---|---|---|
方式一 | var s1 []int | var s2 = []int{} |
方式二 | var s4 = *new([]int) | var s3 = make([]int, 0) |
长度 | 0 | 0 |
容量 | 0 | 0 |
和 nil 比较 | true | false |
json序列化 | null | [] |
官方标准建议:避免写代码的时候把脑袋搞昏的最好办法是不要创建「 空切片」,统一使用「 nil 切片」,同时要避免将切片和 nil 进行比较来执行某些逻辑。
1. 下标方式
编译器转化成OpSliceMake操作。SliceMake 操作会接受四个参数-参数类型、数组指针地址、切片大小和容量。
注意:下标初始化切片不会拷贝原数组或者原切片中的数据,它只会创建一个指向原数组的切片结构体,所以—修改新切片的数据也会修改原切片。
2. 字面量方式
[]int{1,2,3} 创建新切片, cmd/compile/internal/gc.slicelit 函数操作步骤
3. 关键字-make
调用make关键字,传入切片大小、容量、然后进行类型检查。
类型检查期间进行校验入参:
/src/cmd/compile/internal/gc/typecheck.go#L326
# 1.切片的大小和容量是否足够小
# 2.切片是否发生了逃逸,最终在堆上初始化
func typecheck1(n *Node, top int) (res *Node) {
...
}
如果切片发生了逃逸,运行时需要runtime.makeslice在堆上初始化切片。如果非常小不逃逸直接下标方式
访问元素
切片的基本操作都是在编译期间完成的。
追加和扩容
追加扩容分为两种逻辑,append返回的新切片需不需要赋值会原有的变量。两种逻辑差不多,差距点在于是否需要赋值回原有变量。
# /src/runtime/slice.go#L125
# 以下部分代码仅确定大致容量
func growslice(et *_type, old slice, cap int) slice {
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
if newcap <= 0 {
newcap = cap
}
}
}
扩容策略:
- 如果期望容量大于当前容量的两倍就会使用期望容量;
- 如果当前切片的长度小于 1024 就会将容量翻倍;
- 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;
如果切片中元素所占字节为1、8或者2的倍数时。然后再进行内存对齐。对齐一般是内存向上取整。
拷贝切片
copy(a,b) —-> cmd/compile/internal/gc/walk.go-copyany函数分为两种情况
- 当前copy不是运行时调用的。-编译期间拷贝
- 拷贝是在运行时发生的。
打切片拷贝操作时一定要注意对性能的影响。
小结
切片的很多功能斗士由运行时实现的。 切片初始化、切片追加或扩容都需要运行时操作。 大切片扩容活复制或者大规模内存拷贝是,一定要减少类似操作,避免影响程序的性能。