切片
切片
在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
函数接收三个参数:类型,长度,容量。举个例子解释一下长度与容量的区别,假设有一桶水,水并不是满的,桶的高度就是桶的容量,代表着总共能装多少高度的水,而桶中水的高度就是代表着长度,水的高度一定小于等于桶的高度,否则水就溢出来了。所以,切片的长度代表着切片中元素的个数,切片的容量代表着切片总共能装多少个元素,切片与数组最大的区别在于切片的容量会自动扩张,而数组不会,更多细节前往参考手册 - 长度与容量。
提示
切片的底层实现依旧是数组,是引用类型,可以简单理解为是指向底层数组的指针。
通过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 - 二维切片
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]
low
与high
依旧是原来的含义不变,而多出来的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
那么这么做就会有一个明显的问题,s1
与s2
是共享的同一个底层数组,在对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)
}
限制了切割后的容量,这样可以避免覆盖原切片的后续元素。