Golang的数组与切片

大部分编程语言都提供了数组类型的数据结构,数组是非常重要的数据结构。Golang除了提供数组,同时提供了一个跟数组类似的数据结构 - 切片(Slice),相对于Golang中的数组,切片功能更加强大,拥有更好的扩展性能

数组

数组是固定长度的一片连续的内存区域,数组的长度不能修改,需要在初始化的时候指定长度

声明与创建

数组有三种声明方式

1
2
3
var arr [5]int
var arr = [5]int{1,2,3,4,5}
arr := [...]int{1,2,3,4}

第三种声明方式本质上是一个语法糖,在编译阶段会自动推断数组的长度

数组的值复制

区别于C以及大多数编程语言,Go语言中数组在赋值和函数调用过程中的参数传递都是值复制,这里需要特别注意,习惯了其它语言的引用类型的数组,在Go编程过程中很容易出现错误

1
2
3
4
5
a1 := [3]int{1,2,3}
a2 := a1
a2[0] = 0
// a1[0] is: 1, a2[0] is: 0
fmt.Printf("a1[0] is: %d, a2[0] is: %d\n", a1[0], a2[0])

值复制有一定的资源开销,数组作为参数传递过程中,如果数组本身比较大,值复制会有性能问题,这个时候建议使用数组指针,或者其它方案代替

切片

切片跟数组类似,但是切片提供了动态扩容的功能

数据结构

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

切片数据结构有三个字段,指向底层数组的指针,切片长度和切片容量。
切片在创建或者扩容过程中可以预留一部分空间用于添加新的元素,当容量不足时通过扩容的方式添加,切片长度记录了切片中实际存储的元素数量,容量标识当前底层数组最大能存储的元素数量

声明与创建

除了字面量创建切片外,还可以通过make函数创建切片,并指定长度和容量

1
2
3
var arr []int
var arr = []int{1,2,3,4,5}
arr := make([]int, 10, 20)

可以通过内置函数len获取切片长度,cap获取切片的容量

切片的值复制

跟数组一样,切片本身也是值复制,由于切片本身不存储数据,只是保存了一个数组指针,在切片被复制的时候,底层数组不会被复制,被复制的是数组指针

1
2
3
4
5
s1 := []int{1,2,3}
s2 := s1
s2[0] = 0
// s1[0] is: 0, s2[0] is: 0
fmt.Printf("s1[0] is: %d, s2[0] is: %d\n", s1[0], s2[0])

复制指针要比复制数组元素简单的多,所以切片的复制比数组复制性能更好。

如果希望完整的复制切片数据,避免共用底层数组结构,可以使用内置copy函数进行切片深度复制。

切片操作

切片支持截取操作,截取后的切片长度和容量都会发生变化

1
2
3
s1 := make([]int, 10, 20)
s2 := s1[3:5]
fmt.Printf("len: %d, cap: %d\n", len(s2), cap(s2)) // len: 2, cap: 17

截取后的切片长度跟截取的元素数量一直,容量截取起始位置一直到末尾
截取后的切片跟原始切片共用同一个底层数组结构,只是指针指向的地址不同

1
2
3
4
s1 := make([]int, 10, 20)
s2 := s1[3:5]
s2[0] = 4
fmt.Printf("s1[3]: %d, s2[0]: %d\n", s1[3], s2[0]) // s1[3]: 4, s2[0]: 4

收缩与扩容

切片可以通过append操作添加元素,当切片有足够的容量添加元素时,切片只需要把元素放到第一个空闲位置即刻,不需要重新申请内容,当切片容量不足时,切片会重新申请一片内存区域,并把旧的元素通过内存复制的方式全部拷贝过来,新的内存区域会根据一定的规则预留出一部分容量以备新的元素加入

切片的截取操作可以删除切片头部或者尾部的一个或多个元素,如果想删除中间位置的多个元素,可以通过先截取,然后append合并,这种方式既优雅又高效

切片的元素删除并不会回收内存空间,使用过程中不要频繁的做头部删除操作,防止内存泄露