Go

切片和数组详解

2020-04-14  本文已影响0人  sarto

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 开发者们也给我们实现了优秀的方案,感兴趣的可以自行探索。

总结

关于数组和切片的基本概念和实现,到此全部理清。总结一下数组和切片中重点。

上一篇下一篇

猜你喜欢

热点阅读