Golang数组与切片

Posted by 淦 Blog on January 24, 2023

数组

数组是内存中一片连续的区域,需要在初始化时被指定长度,数组的大小取决于数组中存放的元素大小。

1
2
3
4
// 数组声明的三种方式
var arr [3]int
var arr2 = [4]int{1, 2, 3, 4}
arr3 := [...]int{2, 3, 4}

对于数组还有一种语法糖,可以不用指定类型,如arr3 := […]int{2,3,4},这种声明方式在编译时自动推断长度。

Go语言中的数组在赋值和函数调用时的形参都是值复制。

切片

切片是长度可变的序列。序列中的每个元素都有相同的类型。一个切片一般写作[]T, 其中T代表slice中元素的类型。和数组不同的是,切片不用指定固定长度。

使用方法

结构

切片是一种轻量级的数据结构,提供了访问数组任意元素的功能,一个切片在运行时由指针 (data) 、长度(len)和容量(cap) 3部分构成。

1
2
3
4
5
type sliceHeader struct{
    Data uintptr
    Len    int
    Cap   int
}

指针指向切片元素对应的底层数组元素的地址。长度对应切片中元素的数目,长度不能超过容量。容量般是从切片的开始位置到底层数据的结尾位置的长度。

初始化

切片有多种声明方式,在只声明不赋初始值的情况下,切片silce的值为预置的nil,切片的初始化需要使用内置的make函数。

1
2
3
4
var slice []int
var slice2 []int = make([]int,5)
var slice3 []int = make([]int,5,7)
numbers := []int{1,2,3,4,5,6,7,8}

切片有长度和容量的区别,可以在初始化时指定。由于切片具有可扩展性,所以当它的容量比长度大时,意味着为切片元素的增长预留了内存空间。上例中slice2指定了长度为5的int切片,如果不指定容量,则默认其容量与长度相同。numbers被称为切片字面量,在初始化阶段对切片进行了赋值。编译器会自动推断出切片初始化的长度,并使其容量与长度相同。

截取

操作 含义
s[low:high] 从切片s的索引位置low到high处所获得的切片,len=high-low,cap=cap(s)-low
s[low : high : max] 从切片s的索引位置low到high处所获得的切片,len=high-low,cap=max-low

注:

  • max表示切片的最大下标,且low<=high<=len(s)<=max<=cap(s)
  • 被截取后的数组仍然指向原始切片的底层数据。

值复制与数据引用

切片的复制其实也是值复制,但这里的值复制指对于运行时SliceHeader结构的复制。底层指针仍然指向相同的底层数据的数组地址,因此可以理解为数据进行了引用传递。切片的这一特性使得即便切片中有大量数据,在复制时的成本地也比较小,这与数组有显著的不同。

底层原理

扩容原理

  • 如果新申请容量(cap)大于2倍的旧容量(old.cap) ,则最终容量(newcap) 是新申请的容量(cap)。
  • 如果旧切片的长度小于1024,则最终容量是旧容量的2倍,即newcap=doublecap。
  • 如果旧切片长度大于或等于1024, 则最终容量从旧容量开始循环增加原来的1/4,即 newcap=old.cap,for {newcap += newcap/4),直到最终容量大于或等于新申请的容量为止,即newcap≥cap.
  • 如果最终容量计算值溢出,即超过了int的最大范围,则最终容量就是新申请容量。
  • 扩容后新的切片不一定拥有新的地址。

完整复制

复制的切片不会改变指向底层的数据源,但有些时候希望建一个新的数组,并且与旧数组不共享相同的数据源,这时可以使用copy函数。

虽然比较少见,但是切片的数据可以复制到数组中,如果在复制时,数组的长度与切片的长度不同,则复制的元素为len(arr)与len(slice)的较小值