概述
切片 slice 序列的长度可以变化,可以往切片里新增,或删除元素。
切片实际上由两部分组成:结构体
+ 实际存储数据的数组
切片结构体信息
结构体里有三个属性,用于存储一些必要信息:
slice[0]
元素的指针地址,也就是切片第 1 个元素所在的底层数组位置- 切片的长度,表示已经有值的数组
- 容量,表示预先分配好的数组内存空间
底层数组
实际存储数据的数组,通过指针和切片结构体联系在一起。
声明
var 关键字
切片的声明也挺简单,如下
// 声明string类型的切片
var cityList []string
但是需要注意的是,因为切片是引用类型,声明时内存中还没有引用数组,所以此时的零值默认是 nil
var cityList []string
if cityList == nil {
fmt.Println("nil")// 将会输出
}
还有一种声明方式,如下所示,它们是等价的,第二种方式
func main() {
// 方式1
var a []int
// 方式2
b := []int(nil) // 等效方式1
c := append([]int(nil), 1) // 可以不用var关键字声明后再使用
fmt.Println(a == nil, b == nil, c)
}
切片表达式
如下所示,声明一个 months 月份数组
var months = [...]string{
1: "一月",
2: "二月",
3: "三月",
4: "四月",
5: "五月",
6: "六月",
7: "七月",
8: "八月",
9: "九月",
10: "十月",
11: "十一月",
12: "十二月",
}
在 months 数组中,一月份是 months[1]
,十二月份是 months[12]
。通常,数组的第一个元素从索引 0 开始,但是月份一般是从 1 开始的,因此我们声明数组时直接跳过第 0 个元素,第 0 个元素会被自动初始化为空字符串。
slice 的切片操作 s[i:j]
:
i
和j
都数组的下标,遵循左闭右开的原则,- 新的 slice 将只有
j-i
个元素。 - 如果
i
省略,就是 0,表示从头开始切 - 如果
j
省略,就是数组长度,表示切到数组末尾。
因此,举例说明:
months[1:13]
切片操作将引用全部有效的月份,和months[1:]
操作等价;months[:]
切片操作则是引用整个数组。
让我们分别定义表示第二季度和北方夏天月份的 slice,它们有重叠部分:
Q2 := months[4:7]
summer := months[6:9]
fmt.Println(Q2) //[四月 五月 六月]
fmt.Println(summer) // [六月 七月 八月]
如果切片操作超出 cap 上限将导致一个 panic 异常
aa := months[:14]
fmt.Println(aa) // panic invalid argument: index 14 out of bounds [0:14]
可以对切片再进行切片操作,只要不要超过该切片的 cap 就可以
endlessSummer := summer[:5] // extend a slice (within capacity)
fmt.Println(endlessSummer) // "[June July August September October]"
可以指定切片的最大上限,表达式是 a[low : high : max]
,此时得到的结果切片的容量为 max-low
。
a := [5]int{1, 2, 3, 4, 5}
t := a[1:3:5]
// 5-1 = 4,所以容量是4
fmt.Printf("t:%v len(t):%v cap(t):%v\n", t, len(t), cap(t))
初始化
定义的时候使用字面量的形式初始化
var a = []int{1,2,3}
使用 make 函数构造切片的时候,也会自动初始化
a := make([]int, 2, 10)
获取切片的元素值
基本上和数组访问元素是一样的,都是通过下标进行访问,但是需要注意的是,切片如果没有初始化的话,则不允许访问
var a []int // 没有初始化,在内存中没有位置
a[0] = 1 // 编译没问题,运行的时候会报错
b := []int{1, 2} // 初始化
b[2] = 1 // 可以正常设置
切片之间进行比较
切片之间是不能比较,不能使用 ==
来判断两个切片是否含有全部相等元素,唯一合法的比较操作是和 nil
比较。
- 一个
nil
值的切片并没有底层数组(也就是没有初始化) - 一个
nil
值的切片的长度和容量都是 0 - 不能说一个长度和容量都是 0 的切片一定是
nil
var s1 []int //len(s1)=0;cap(s1)=0;s1==nil
s2 := []int{} //len(s2)=0;cap(s2)=0;s2!=nil
s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil
始终使用 len(s) == 0
来判断一个切片是否为空,而不应该使用 s == nil
来判断,或者需要自己遍历切片,一个个元素去比较。
切片赋值拷贝
切片是引用类型,赋值后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容
s1 := make([]int, 3) //[0 0 0]
s2 := s1 //将s1直接赋值给s2,s1和s2共用一个底层数组
s2[0] = 100 // 修改切片元素值
fmt.Println(s1) //[100 0 0]
fmt.Println(s2) //[100 0 0]
copy 的时候,可以copy部分数据,以下是gin框架里的源码
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
assert1(finalSize < int(abortIndex), "too many handlers")
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
// copy 到后半部分
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
因为切片其实就是一个结构体,里面有指针指向底层数组,所以在函数里修改底层数组,外层也会改变
func setHobby(hobby []string) bool {
hobby[1] = "bb"
return true
}
func main() {
hobby := []string{"a", "b", "c"}
setHobby(hobby)
fmt.Printf("%#v", hobby) // []string{"a", "bb", "c"}
}
但是如果是 append 的操作,或 delete 操作的话,需要传递指针,不然不会改变,因为此时的切片结构体的 len 已经变成了 4 了,已经不是原来 aa 这个切片结构体了,如下图,append 操作后,arr 和 brr 不是同一个了,它们的长度不一样
func AppendEle(s []int) {
s = append(s, 9)
}
func RemoveEle(s []int) {
s = (s)[0:1]
}
func main() {
aa := make([]int, 3)
AppendEle(aa)
fmt.Println(aa) // [0 0 0]
RemoveEle(aa)
fmt.Println(aa) // [0 0 0]
}
需要改成切片结构体改成指针类型,在函数里修改切片结构体变量内容,也就长度会被修改
func AppendEle(s *[]int) {
*s = append(*s, 9)
}
func RemoveEle(s *[]int) {
*s = (*s)[0:1]
}
func main() {
aa := make([]int, 3)
AppendEle(&aa)
fmt.Println(aa) // [0 0 0]
RemoveEle(&aa)
fmt.Println(aa) // [0]
}
如果想要拷贝一份独立的数据,需要用 copy 函数
// copy()复制切片
a := []int{1, 2, 3, 4, 5}
c := make([]int, 5, 5)
copy(c, a) //使用copy()函数将切片a中的元素复制到切片c
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1 2 3 4 5]
c[0] = 1000
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1000 2 3 4 5],不会改变a切片
往切片里增加元素
使用 append()
函数,可以为切片动态添加元素:
- 返回值必须赋值给一个接受变量
- 底层数组容量不足时,append 函数会自动按照一定的策略进行"扩容"
- 尽量避免扩容,减少性能消耗
- 不能通过索引下标的方式进行扩容
sli := make([]int, 2, 3)
sli[2] = 4 // 运行的时候将报错,因为长度只有2,索引不能超过1
fmt.Println(sli)
通过 var 声明的零值切片可以在 append()
函数直接使用,无需初始化
var s []int // 无需初始化
s = append(s, 1)
fmt.Println(s)
对原来数进行切片的扩容,好像不会影响原来数组本身,因为会重新 make 一个新的切片,并且容量是它的两倍
var array = [5]int{1, 2, 3, 4, 5}
s := array[3:]
s = append(s, 6)
s[0] = 100
fmt.Println(array) // [1 2 3 4 5],原来的数组不会受到影响
fmt.Println(s) //[4 5 6]
fmt.Println(len(s)) //3
fmt.Println(cap(s)) //4
对数组进行切片后,修改切片的话,会影响原来的数组
var array = [5]int{1, 2, 3, 4, 5}
s := array[3:]
s[0] = 5555
fmt.Println(array) // [1 2 3 5555 5]
还可以可以一次性增加多个元素
var s []int
s = append(s, 1, 2, 3, 4, 5, 6)
fmt.Println(s)
添加另一个切片中的元素,注意要加 ...
var s []int
s1 := []int{1, 2, 3, 4, 5} // 需要初始化
s = append(s, s1...) // 后面需要添加...
fmt.Println(s)
删除元素
Go 本身没有删除切片元素的专用方法,可以使用切片本身的特性来删除元素
// 从切片中删除元素
a := []int{30, 31, 32, 33, 34, 35, 36, 37}
// 要从切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]...)
// 要删除索引为2的元素
a = append(a[:2], a[3:]...)
fmt.Println(a) //[30 31 33 34 35 36 37]
排序
slices.SortFunc(remoteDataList, func(a, b remoteDataType) bool {
if extra.IsReverse {
return a.CommitTime < b.CommitTime
} else {
return b.CommitTime < a.CommitTime
}
})
面试题
下面代码的输出是什么
func main() {
var arr [3]int
brr := arr[1:2]
brr = append(brr, 9)
brr = append(brr, 9)
fmt.Println(arr, brr, cap(brr)) // [0 0 9] [0 9 9] 4
}
这个切片可以理解为是个结构体,里面有三个属性:
- 第一个是底层数组的首地址
- 第二个是长度,目前切片里的元素,也就是黄色的那个
- 第三是容量,容量是2,切片截取是从1开始截取,1之后的都是容量
然后 append(brr,9) 的操作,会改掉底层数组绿色的值,因为这个还是切片的容量
然后在来个 append(brr,9) 的操作,这个时候,底层数组的容量不够了,需要新申请一份数组内存,容量是原来的两倍,原来是2,那现在新的就是4,然后把原来的0和9拷贝到新的数组这边过来,接着再这新的数组这边append,所以如下所示