切片

寒江蓑笠翁大约 20 分钟

切片

在Go中,数组和切片两者看起来长得几乎一模一样,但功能有着不小的区别,数组是定长的数据结构,长度被指定后就不能被改变,而切片是不定长的,切片在容量不够时会自行扩容。

数组

如果事先就知道了要存放数据的长度,且后续使用中不会有扩容的需求,就可以考虑使用数组,Go中的数组是值类型,而非引用,并不是指向头部元素的指针。

提示

数组作为值类型,将数组作为参数传递给函数时,由于Go函数是传值传递,所以会将整个数组拷贝。

初始化

数组在声明是长度只能是一个常量,不能是变量

// 正确示例
var a [5]int

// 错误示例
l := 1
var b [l]int

先来初始化一个长度为5的整型数组

var nums [5]int

也可以用元素初始化

nums := [5]int{1, 2, 3}

可以让编译器自动推断长度

nums := [...]int{1, 2, 3, 4, 5} //等价于nums := [5]int{1, 2, 3, 4, 5},省略号必须存在,否则生成的是切片,不是数组

还可以通过new函数获得一个指针

nums := new([5]int)

以上几种方式都会给nums分配一片固定大小的内存,区别只是最后一种得到的值是指针。


在数组初始化时,需要注意的是,长度必须为一个常量表达式,否则将无法通过编译,常量表达式即表达式的最终结果是一个常量,错误例子如下:

length := 5 // 这是一个变量
var nums [length]int

length是一个变量,因此无法用于初始化数组长度,如下是正确示例:

const length = 5
var nums [length]int // 常量
var nums2 [length + 1]int // 常量表达式
var nums3 [(1 + 2 + 3) * 5]int // 常量表达式
var nums4 [5]int // 最常用的

使用

只要有数组名和下标,就可以访问数组中对应的元素。

fmt.Println(nums[0])

同样的也可以修改数组元素

nums[0] = 1

还可以通过内置函数len来访问数组元素的数量

len(nums)

内置函数cap来访问数组容量,数组的容量等于数组长度,容量对于切片才有意义。

cap(nums)

切割

切割数组的格式为arr[startIndex:endIndex],切割的区间为左闭右开,例子如下:

nums := [5]int{1, 2, 3, 4, 5}
nums[1:] // 子数组范围[1,5) -> [2 3 4 5]
nums[:5] // 子数组范围[0,5) -> [1 2 3 4 5]
nums[2:3] // 子数组范围[2,3) -> [3]
nums[1:3] // 子数组范围[1,3) -> [2 3]

数组在切割后,就会变为切片类型

func main() {
	arr := [5]int{1, 2, 3, 4, 5}
	fmt.Printf("%T\n", arr)
	fmt.Printf("%T\n", arr[1:2])
}

输出

[5]int
[]int

若要将数组转换为切片类型,不带参数进行切片即可,转换后的切片与原数组指向的是同一片内存,修改切片会导致原数组内容的变化

func main() {
	arr := [5]int{1, 2, 3, 4, 5}
	slice := arr[:]
	slice[0] = 0
	fmt.Printf("array: %v\n", arr)
	fmt.Printf("slice: %v\n", slice)
}

输出

array: [0 2 3 4 5]
slice: [0 2 3 4 5]

如果要对转换后的切片进行修改,建议使用下面这种方式进行转换

func main() {
	arr := [5]int{1, 2, 3, 4, 5}
	slice := slices.Clone(arr[:])
	slice[0] = 0
	fmt.Printf("array: %v\n", arr)
	fmt.Printf("slice: %v\n", slice)
}

输出

array: [1 2 3 4 5]
slice: [0 2 3 4 5]

切片

切片在Go中的应用范围要比数组广泛的多,它用于存放不知道长度的数据,且后续使用过程中可能会频繁的插入和删除元素。


初始化

切片的初始化方式有以下几种

var nums []int // 值
nums := []int{1, 2, 3} // 值
nums := make([]int, 0, 0) // 值
nums := new([]int) // 指针

可以看到切片与数组在外貌上的区别,仅仅只是少了一个初始化长度。通常情况下,推荐使用make来创建一个空切片,只是对于切片而言,make函数接收三个参数:类型,长度,容量。举个例子解释一下长度与容量的区别,假设有一桶水,水并不是满的,桶的高度就是桶的容量,代表着总共能装多少高度的水,而桶中水的高度就是代表着长度,水的高度一定小于等于桶的高度,否则水就溢出来了。所以,切片的长度代表着切片中元素的个数,切片的容量代表着切片总共能装多少个元素,切片与数组最大的区别在于切片的容量会自动扩张,而数组不会,更多细节前往参考手册 - 长度与容量open in new window

提示

切片的底层实现依旧是数组,是引用类型,可以简单理解为是指向底层数组的指针。

通过var nums []int这种方式声明的切片,默认值为nil,所以不会为其分配内存,而在使用make进行初始化时,建议预分配一个足够的容量,可以有效减少后续扩容的内存消耗。

使用

切片的基本使用与数组完全一致,区别只是切片可以动态变化长度,下面看几个例子。

切片可以通过append函数实现许多操作,函数签名如下,slice是要添加元素的目标切片,elems是待添加的元素,返回值是添加后的切片。

func append(slice []Type, elems ...Type) []Type

首先创建一个长度为0,容量为0的空切片,然后在尾部插入一些元素,最后输出长度和容量。

nums := make([]int, 0, 0)
nums = append(nums, 1, 2, 3, 4, 5, 6, 7)
fmt.Println(len(nums), cap(nums)) // 7 8 可以看到长度与容量并不一致。

新 slice 预留的 buffer容量 大小是有一定规律的。 在golang1.18版本更新之前网上大多数的文章都是这样描述slice的扩容策略的: 当原 slice 容量小于 1024 的时候,新 slice 容量变成原来的 2 倍;原 slice 容量超过 1024,新 slice 容量变成原来的1.25倍。 在1.18版本更新之后,slice的扩容策略变为了: 当原slice容量(oldcap)小于256的时候,新slice(newcap)容量为原来的2倍;原slice容量超过256,新slice容量newcap = oldcap+(oldcap+3*256)/4

插入元素

切片元素的插入也是需要结合append函数来使用,现有切片如下,

nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

从头部插入元素

nums = append([]int{-1, 0}, nums...)
fmt.Println(nums) // [-1 0 1 2 3 4 5 6 7 8 9 10]

从中间下标i插入元素

nums = append(nums[:i+1], append([]int{999, 999}, nums[i+1:]...)...)
fmt.Println(nums) // i=3,[1 2 3 4 999 999 5 6 7 8 9 10]

从尾部插入元素,就是append最原始的用法

nums = append(nums, 99, 100)
fmt.Println(nums) // [1 2 3 4 5 6 7 8 9 10 99 100]

删除元素

切片元素的删除需要结合append函数来使用,现有如下切片

nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

从头部删除n个元素

nums = nums[n:]
fmt.Println(nums) //n=3 [4 5 6 7 8 9 10]

从尾部删除n个元素

nums = nums[:len(nums)-n]
fmt.Println(nums) //n=3 [1 2 3 4 5 6 7]

从中间指定下标i位置开始删除n个元素

nums = append(nums[:i], nums[i+n:]...)
fmt.Println(nums)// i=2,n=3,[1 2 6 7 8 9 10]

删除所有元素

nums = nums[:0]
fmt.Println(nums) // []

拷贝

切片在拷贝时需要确保目标切片有足够的长度,例如

func main() {
	dest := make([]int, 0)
	src := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
	fmt.Println(src, dest)
	fmt.Println(copy(dest, src))
	fmt.Println(src, dest)
}
[1 2 3 4 5 6 7 8 9] []
0                     
[1 2 3 4 5 6 7 8 9] []

将长度修改为10,输出如下

[1 2 3 4 5 6 7 8 9] [0 0 0 0 0 0 0 0 0 0]
9                                        
[1 2 3 4 5 6 7 8 9] [1 2 3 4 5 6 7 8 9 0]

遍历

切片的遍历与数组完全一致,for循环

func main() {
   slice := []int{1, 2, 3, 4, 5, 7, 8, 9}
   for i := 0; i < len(slice); i++ {
      fmt.Println(slice[i])
   }
}

for range循环

func main() {
	slice := []int{1, 2, 3, 4, 5, 7, 8, 9}
	for index, val := range slice {
		fmt.Println(index, val)
	}
}

多维切片

先来看下面的一个例子,官方文档也有解释:Effective Go - 二维切片open in new window

var nums [5][5]int
for _, num := range nums {
   fmt.Println(num)
}
fmt.Println()
slices := make([][]int, 5)
for _, slice := range slices {
   fmt.Println(slice)
}

输出结果为

[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]

[]
[]
[]
[]
[]

可以看到,同样是二维的数组和切片,其内部结构是不一样的。数组在初始化时,其一维和二维的长度早已固定,而切片的长度是不固定的,切片中的每一个切片长度都可能是不相同的,所以必须要单独初始化,切片初始化部分修改为如下代码即可。

slices := make([][]int, 5)
for i := 0; i < len(slices); i++ {
   slices[i] = make([]int, 5)
}

最终输出结果为

[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]

[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]

拓展表达式

提示

只有切片才能使用拓展表达式

切片与数组都可以使用简单表达式来进行切割,但是拓展表达式只有切片能够使用,该特性于Go1.2版本添加,主要是为了解决切片共享底层数组的读写问题,主要格式为如下,需要满足关系low<= high <= max <= cap,使用拓展表达式切割的切片容量为max-low

slice[low:high:max]

lowhigh依旧是原来的含义不变,而多出来的max则指的是最大容量,例如下方的例子中省略了max,那么s2的容量就是cap(s1)-low

s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4] // cap = 9 - 3 = 6

那么这么做就会有一个明显的问题,s1s2是共享的同一个底层数组,在对s2进行读写时,有可能会影响的s1的数据,下列代码就属于这种情况

s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4]                          // cap = 9 - 3 = 6
// 添加新元素,由于容量为6.所以没有扩容,直接修改底层数组
s2 = append(s2, 1)
fmt.Println(s2)
fmt.Println(s1)

最终的输出为

[4 1]
[1 2 3 4 1 6 7 8 9]

可以看到明明是向s2添加元素,却连s1也一起修改了,拓展表达式就是为了解决此类问题而生的,只需要稍微修改一下就能解决该问题

func main() {
   s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
   s2 := s1[3:4:4]                        // cap = 4 - 3 = 1
   // 容量不足,分配新的底层数组
   s2 = append(s2, 1)
   fmt.Println(s2)
   fmt.Println(s1)
}

现在得到的结果就是正常的

[4 1]
[1 2 3 4 5 6 7 8 9]

clear

在go1.21新增了clear内置函数,clear会将切片内所有的值置为零值,

package main

import (
    "fmt"
)

func main() {
    s := []int{1, 2, 3, 4}
    clear(s)
    fmt.Println(s)
}

输出

[0 0 0 0]

如果想要清空切片,可以

func main() {
	s := []int{1, 2, 3, 4}
    s = s[:0:0]
	fmt.Println(s)
}

限制了切割后的容量,这样可以避免覆盖原切片的后续元素。