切片基础篇
1. 定义
切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组
类型做的一层封装。它非常灵活,支持自动扩容。
切片是一个引用类型,它的内部结构包含地址
、长度
和容量
。切片一般用于快速地操作一块数据集合。
var sint []int // 定义一个int类型的切片
var sstr []string // 定义一个string类型切片
fmt.Println(sint, sstr)
fmt.Println(sint == nil, sstr == nil)//nil ,其实没有分配内存的,初始化才能使用!数组有默认值!
out
[] []
true true
从上面可以看到,切片和数组直观上的不同
- 切片定义的时候是没有长度的,而数组必须有长度,且是数组类型的一部分
- 切片没有默认值,而数组是有默认值的
- 切片可可以使用
make()
初始化,数组不可以
2. 切片初始化
sint = []int{1, 2, 3}
sstr = []string{"江宁", "雨花台", "鼓楼", "栖霞", "建邺"}
fmt.Println(sint)
fmt.Println(sstr)
out
[1 2 3]
[江宁 雨花台 鼓楼 栖霞 建邺]
3. 初识len()和cap()
切片的容量是底层数组从切片的第一个元素指向s数组的最后元素的这个长度。和长度是不一样的
fmt.Println(len(sint), cap(sint))
fmt.Println(len(sstr), cap(sstr))
out
3 3
5 5
4. 由数组得到切片
这个和python的是类似的
aint := [...]int{1, 3, 5, 7, 9, 11, 13, 15}
s1 := aint[0:4] // 闭右开,类似py里的列表
s2 := aint[1:6]
s3 := aint[:4]
s4 := aint[3:]
s5 := aint[:]
fmt.Printf("s1=%v\n", s1)
fmt.Printf("s2=%v\n", s2)
fmt.Printf("s3=%v\n", s3)
fmt.Printf("s4=%v\n", s4)
fmt.Printf("s5=%v\n", s5)
out
s1=[1 3 5 7]
s2=[3 5 7 9 11]
s3=[1 3 5 7]
s4=[7 9 11 13 15]
s5=[1 3 5 7 9 11 13 15]
5. 切片的长度和容量
切片的容量是底层数组从切片的第一个元素指向数组的最后元素的这个长度。和长度是不一样的
// 接上一段代码
fmt.Printf("len(s1)=%d, cap(s1)=%d\n", len(s1), cap(s1))
fmt.Printf("len(s4)=%d, cap(s4)=%d\n", len(s4), cap(s4))
fmt.Printf("len(s5)=%d, cap(s5)=%d\n", len(s5), cap(s5))
fmt.Printf("len(s3)=%d, cap(s3)=%d\n", len(s3), cap(s3))
out
len(s1)=4, cap(s1)=8
len(s4)=5, cap(s4)=5
len(s5)=8, cap(s5)=8
len(s3)=4, cap(s3)=8
6. 切片再切片
这种情况下,切片的容量计算方式也是一样的。
s6 := s2[0:3]
fmt.Printf("s6=%v\n", s6)
fmt.Printf("len(s6)=%d,cap(s6)=%d\n", len(s6), cap(s6))
out
s6=[3 5 7]
len(s6)=3,cap(s6)=7
7. 切片是引用类型
切片是引用类型,都指向了底层的一个数组,底层变了,它也变
// 接上面代码
aint[2] = 1300
fmt.Printf("s6=%v\n", s6)
out
s6=[3 1300 7]
8. 使用make()函数构造切片
make([]T, size, cap)
其中:
- T:切片的元素类型
- size:切片中元素的数量
- cap:切片的容量
make 初始化,是分配内存的,不为nil,go语言毕竟偏向底层,指针,长度,容量,后面会用的多!
s1 := make([]int, 5, 10) // 5是长度,10是容量
s2 := make([]int, 0, 10) // 底层数组的长度是10
fmt.Printf("s1=%v,len(s1)=%d,cap(s1)=%d\n", s1, len(s1), cap(s1))
fmt.Printf("s2=%v,len(s2)=%d,cap(s2)=%d\n", s2, len(s2), cap(s2))
out
s1=[0 0 0 0 0],len(s1)=5,cap(s1)=10
s2=[],len(s2)=0,cap(s2)=10
注意使用make初始化切片时,第二个元素一般是0,不然前面是有默认元素的。可以参考demo~切片review
9. 共享底层数组
下面的代码中演示了拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容,这点需要特别注意。
s3 := []int{1, 3, 5}
s4 := s3 // 切片的赋值,s2和s4都指向了同一个底层数组,也就是同一个内存地址,本身是不存放数据的
fmt.Println(s3, s4)
s3[0] = 100
fmt.Println(s3, s4)
out
[1 3 5] [1 3 5]
[100 3 5] [100 3 5]
10. 遍历
切片的遍历和数组遍历是一样的,都可以通过索引
或是for range
的方式进行遍历
- 索引遍历
for i := 0; i < len(s3); i++ {
fmt.Println(s3[i])
}
- for range 遍历
for _, v := range s3 {
fmt.Println(v)
}
11. append()函数扩容切片
Go语言的内建函数append()
可以为切片动态添加元素。 可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)。
s1 := []string{"江宁", "鼓楼", "雨花台"}
fmt.Printf("s1=%v,len(s1)=%d,cap(s1)=%d\n", s1, len(s1), cap(s1))
// 调用append函数必须使用原来的切片变量接收返回值
s1 = append(s1, "建邺")
fmt.Printf("s1=%v,len(s1)=%d,cap(s1)=%d\n", s1, len(s1), cap(s1))
//添加多个
s1 = append(s1, "栖霞", "连云港")
fmt.Printf("s1=%v,len(s1)=%d,cap(s1)=%d\n", s1, len(s1), cap(s1))
//添加切片
ss := []string{"北京", "上海", "广州"}
s1 = append(s1, ss...)
fmt.Printf("s1=%v,len(s1)=%d,cap(s2)=%d\n", s1, len(s1), cap(s1))
out
s1=[江宁 鼓楼 雨花台],len(s1)=3,cap(s1)=3
s1=[江宁 鼓楼 雨花台 建邺],len(s1)=4,cap(s1)=6
s1=[江宁 鼓楼 雨花台 建邺 栖霞 连云港],len(s1)=6,cap(s1)=6
append 追加元素,原来的底层数组放不下的时候,go底层会把底层数组换一个
,
上面的cap可以看到,s1的容量第一次追加后扩容到6,但后面再追加就没有变。再在此基础上,增加1个元素,容量扩容到12
/*
1、如果小于1024,2倍扩容
2、如果大于1024,1.25扩容
3、如果申请的容量大于原来的2倍,直接扩容到新申请的容量
4、具体存储的值类型不同,扩容的策略也不同
*/
s1 = append(s1, "bowen")
fmt.Printf("s1=%v,len(s1)=%d cap(s1)=%d\n", s1, len(s1), cap(s1))
out
s1=[江宁 鼓楼 雨花台 建邺 栖霞 连云港 北京 上海 广州 bowen],len(s1)=10 cap(s1)=12
12. append扩容策略
*容量扩容策略,根据不同的数据类型策略不一样。可以看源码,*$GOROOT/*src/runtime/slice.go*
其中扩容相关代码如下
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
从上面的代码可以看出以下内容:
- 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
- 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),
- 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
- 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。
需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如int
和string
类型的处理方式就不一样。
13. copy函数
Go语言内建的copy()
函数可以迅速地将一个切片的数据复制到另外一个切片空间中,注意是新开辟的空间,和第9小节区的共享底层数组分开来
s1 := []int{1, 3, 5}
s2 := s1 // 共享底层数组
var s3 = make([]int, 3, 3)
copy(s3, s1) // 重新开辟空间
fmt.Println(s1, s2, s3)
s1[1] = 100
fmt.Println(s1, s2, s3)
out
[1 3 5] [1 3 5] [1 3 5]
[1 100 5] [1 100 5] [1 3 5]
14. 刪除元素
Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。 代码如下:
// 接上面代碼
s1 = append(s1[:1], s1[2:]...) // s1[2:] 换成s2[2:],也是可以成功删除的
fmt.Println(s1)
out
[1 5]
可以看到,s1第二个元素被删除了。总结一下就是:要从切片a中删除索引为index
的元素,操作方法是a = append(a[:index], a[index+1:]...)
,注意不要写反,写反就不是删除,而是增加了。
补充:删除元素后,底层数组的变化
x1 := []int{1, 3, 5}
fmt.Printf("%T\n", x1)
s1 := x1[:]
fmt.Println(s1, len(s1), cap(s1))
// 指向的内存地址是一样的
fmt.Printf("%p\n", &s1[0])
fmt.Printf("%p\n", &x1[0])
// 删除s1后,
s1 = append(s1[:1], s1[2:]...)
fmt.Println(s1, len(s1), cap(s1))
fmt.Println(x1) // 修改了底层数组[1,5,5] 把2覆盖1了。3没变
fmt.Printf("%p\n", &s1[0])
s1[1] = 100
fmt.Println(x1) //[1,100,5]
out
[]int
[1 3 5] 3 3
0xc00000e120
0xc00000e120
[1 5] 2 3
[1 5 5]
0xc00000e120
[1 100 5]
可以看到一个很奇怪的现象,当我们删除了s1[1]后,s1正常删除,但是x1却不是,这就是删除操作给底层数组带来的影响,底层数组的[2]还是5没有动,但是[1]元素被删除后,底层数组的[1]被[2]覆盖了。
15. 切片面试题
题目1
写出下面程序运行结果
var s = make([]int, 5, 10)
fmt.Println(s) // 这一句是调试加的
for i := 0; i < 10; i++ {
s = append(s, i)
}
fmt.Println(s)
fmt.Println(len(s), cap(s))
out
[0 0 0 0 0]
[0 0 0 0 0 0 1 2 3 4 5 6 7 8 9]
15 20
这题确实是个坑,以为结果会是这样[0 1 2 3 4 5 6 7 8 9]
,然而却是[0 0 0 0 0 0 1 2 3 4 5 6 7 8 9]
,是因为一开始初始化的时候就有默认值了。
题目2
写出下面程序运行结果
func main() {
var s = make([]int, 1)
s = append(s, 1)
fmt.Println(s)
}
out
[0 1]
这题也是个坑!
题目3
func main() {
var s []int
s = append(s, 1)
fmt.Println(s)
}
out
[1]
这题考的是与上面的区别,使用append()
可以初始化切片
16. sort.Ints(s[:])排序
import (
"fmt"
"sort"
)
func main() {
var s = []int{3, 1, 7, 11, 8}
sort.Ints(s[:]) // 是没有返回值的,直接改的就是切片,sort.Ints(s)也是可以的
fmt.Println(s)
}
out
[1 3 7 8 11]
17. 删除切片元素影响底层数组
func main() {
var a = [5]int{1, 3, 5, 7, 9}
s := a[:]
s = append(s[:1], s[2:]...)
fmt.Println(s)
fmt.Println(a)
}
out
[1 5 7 9]
[1 5 7 9 9]
底层数组的最后一个9没有动,其他元素往前移动了。