切片和数组详解
go 语言中的切片是一个常用的数据类型,切片的本质是基于数组的,属于数组之上的抽象数据类型。切片的出现是为了在保持数组的高效性上增加数组不具备的功能,使其更加灵活可用,例如扩容。理解这一点将有助于理解切片的设计思想。
基本概念
在了解切片的性质之前,我们需要先理解数组的本质。
数组
go 语言的数组实现和其他语言都是相同的,具有以下三个基本特性。
- 分配在连续的地址空间
- 元素类型一致
- 空间大小固定,不可修改
正是数组的这种性质,决定了数组高效。所以我们可以简单的用三个参数来定义数组:起始地址,元素大小,容量(元素个数)。
需要注意的是 go 的数组变量属于值语义,传递时是值复制,代表的是整个数组(与 c 不同,c 指向首元素)。这种设计用来体现go语言中数组是一个基本类型
切片
前边提到,切片是在数组之上的抽象类型,为什么叫切片?由于数组是一块固定长度的连续的内存结构,很多时候我们并不能提前预知我们需要多大的存储空间,所以我们希望先使用小一点的空间,并且在需要的时候在进行扩容。此时我们就需要一个切片,切片是在一块给定的数组之上,划出我们需要的区域,这块区域就是一个切片。并且我们能动态的增减可使用范围,直到这个数组达到上限,这样易用性和适用性就强了很多,而其物理实现任然是数组,效率也有了保证。所以我将切片概括为三个参数,就是: 数组,起始位置,终止位置。
需要注意的是 go 的切片属于引用,传递时是传递的变量的值复制,而不会发生底层的数组复制,这种设计思想可以好好体会一下。
数组和切片的初始化
数组
go 对数组的内存分配是编译阶段发生的,因此在运行时不会发生内存的动态分配,实验如下。
func allocArray() {
var _ [1<<20]byte
return
}
func Benchmark_arrayAlloc (b *testing.B){
b.ReportAllocs()
for i:=0;i<b.N; i++{
allocArray()
}
}
benchmark 测试结果
Benchmark_arrayAlloc-4 2000000000 0.30 ns/op 0 B/op 0 allocs/op
可以看到,我们的 allocArray 函数创建了一个 1MB 的数组,但运行时并没有发生内存的动态分配。
切片
切片可以使用 make 函数进行创建初始化,创建时有三个参数,类型,长度,容量。需要注意的是,make 方式的内存分配是运行时发生的,但也不总是,make 函数会被编译器优化,如果分配的字节数很少,则会在编译时发生,所以此处我设置的容量较大为 1MB。同样的我们通过实验进行说明。
func allocSlice(){
_ = make([]byte,1<<20,1<<20)
}
benchmark 测试结果
Benchmark_sliceAlloc-4 10000 101642 ns/op 1048582 B/op 1 allocs/op
可以看到,进行了一次内存的动态分配,1MB 换算 10 进制是1048576。make 函数额外分配了 6 个字节,
这个差距包含测试误差,因为多次测试,这个数据有略微波动。
切片的另一种建立方式
前边我们说过,切片是建立在数组之上的抽象数据类型。所以我们可以手动进行切片的建立而不依赖 make 函数。
func allocSlice2() {
var b [1<<20]byte
_ = b[:]
}
benchmark 测试结果
Benchmark_slice2Alloc-4 2000000000 0.30 ns/op 0 B/op 0 allocs/op
首先,我们创建了一个1MB的数组,然后在这个数组之上创建了一个匿名的切片。可以看到,此时也没有发生内存的动态分配。但出于可读性和更深层次的原因,我们并不采用这种分配方式,只是通过这个例子说明的说明切片的含义。
切片和数组的 Len 与 Cap
go 语言中,len 和 cap 函数可以作用于数组和切片,理解了数组和切片的恩恩怨怨,就很好理解这两个函数的意义。对数组来说,本身是一个固定的数据类型,我认为只有容量,没有长度,对一个数组调用len,返回的结果也用于和cap相同。由于切片是建立在数组之上的抽象数据类型,所以容量代表的就是数组的容量,而始末位置只差,表示这个切片当前的长度。
切片的扩容
切片的扩容有两种情况,数组容量充足和数组容量不足,可以猜想的是,当数组容量充足时,切片只需要移动其始末位置,即可完成扩容,简单而高效。
而数组容量不足时,由于数组不能扩容,所以 go 采用的办法就是抛弃原来的数组,将当前需要扩容的切片建立在一个新的更大数组之上,可以预见,这种方式的消耗必然要大很多。同样一个值得思考的问题是,新的数组分配多大合适?这就是扩容策略的问题,天才的 go 开发者们也给我们实现了优秀的方案,感兴趣的可以自行探索。
总结
关于数组和切片的基本概念和实现,到此全部理清。总结一下数组和切片中重点。
- 数组是一个基本数据类型
- 切片是一个抽象数据类型,其底层实现是数组
- 数组在编译阶段就分配内存
- 切片只有扩容数组时才会运行时分配内存