Go语言基础之切片
1 切片介绍
Golang提供数组这种存储相同类型数据的数据结构,由于在现实生活中一件事物的个数不是固定,比如说一个班级的学生人数等,然而数组的长度是固定,因此在Golang中很少直接使用数组。和数组相对应的类型是切片slice,其代表变长的序列,序列中每个元素都是相同的类型。
1.1 切片的内部实现
如图1所示,可以看出切片的底层还是数组。slice从底层来说,其实就是一个数据结构(struct结构体),切片有3个字段的数据结构,这三个字段分别是指向底层数组的指针、切片访问的元素的个数(即长度)和切片允许增长到的元素个数(即容量)。
图1 切片在内存中的形式
2 切片的使用
2.1 基于数组定义切片
定义一个切片,然后让切片去引用一个已经创建好的数组,如下案例:
1 func main() { 2 // 方式1:基于数组定义切片,定义一个切片,然后让切片去引用已经创建好的数组 3 var arr = [...]int{1, 2, 3, 4, 5} 4 var slice = arr[1:3] //左包含右不包含 5 fmt.Printf("slice的类型%T\n", slice) //slice的类型[]int 6 fmt.Println("slice=", slice) //slice= [2 3] 7 fmt.Println("slice len =", len(slice)) //slice len = 2 8 fmt.Println("slice cap =", cap(slice)) //slice cap = 4 9 }
2.2 使用make()函数创建切片
基本语法: var 切片名 []type = make([]type, len, [cap])
参数说明: type 就是数据类型; len 大小; cap 指定切片容量,可选,如果分配了 cap ,则要求 cap >= len 。
1 func main() { 2 // 方式2:使用make函数创建切片 3 var slice []float64 = make([]float64, 5, 10) 4 slice[1] = 10 5 slice[2] = 20 6 // 对于切片,必须make使用 7 fmt.Println(slice) //[0 10 20 0 0] 8 fmt.Println("slice len =", len(slice)) //slice len = 5 9 fmt.Println("slice cap =", cap(slice)) //slice cap = 10 10 }
- 通过make方式创建切片可以指定切片的大小和容量
- 如果没有给切片的各个元素赋值,那么就会使用默认值
- 通过make方式创建的切片对应的数组是由make底层维护,对外不可见,即只能通过slice去访问各个元素
2.3 直接定义并赋值
定义一个切片,直接就指定具体数组,使用原理类似make的方式
1 func main() { 2 // 方式3:定义一个切片,直接就指定具体数组 3 var slice []string = []string{"tom", "jerry", "mary"} 4 fmt.Println(slice) //[tom jerry mary] 5 fmt.Println("slice len =", len(slice)) //slice len = 3 6 fmt.Println("slice cap =", cap(slice)) //slice cap = 3 7 }
方式一和方式二的区别:
- 方式1是直接引用数组,这个数组是事先存在的,程序员是可见的
- 方式2是通过make来创建切片,make也会创建一个数组,是由切片在底层进行维护,程序员不可见
2.4 切片再切片
除了基于数组和make函数得到切片,我们还可以通过切片来得到切片。
1 func main() { 2 // 切片再切片 3 // 不仅可以基于数组和make函数来创建切片,也可以通过切片创建切片 4 5 cityArray := [...]string{"北京", "上海", "广州", "深圳", "成都", "重庆"} 6 fmt.Printf("cityArray:%v type:%T len:%d cap:%d\n", cityArray, cityArray, len(cityArray), cap(cityArray)) 7 citySlice := cityArray[1:3] 8 fmt.Printf("citySlice:%v type:%T len:%d cap:%d\n", citySlice, citySlice, len(citySlice), cap(citySlice)) 9 newCitySlice := citySlice[1:5] 10 fmt.Printf("newCitySlice:%v type:%T len:%d cap:%d\n", newCitySlice, newCitySlice, len(newCitySlice), cap(newCitySlice)) 11 }
输出:
1 cityArray:[北京 上海 广州 深圳 成都 重庆] type:[6]string len:6 cap:6 2 citySlice:[上海 广州] type:[]string len:2 cap:5 3 newCitySlice:[广州 深圳 成都 重庆] type:[]string len:4 cap:4
切片初始化时,仍然不能越界。范围在[0-len(arr)]之间,但可以动态增长。
- var slice = arr[0:end] 可以简写 var slice = arr[:end]
- var slice = arr[:end] 可以简写 var slice = arr[:end]
- var slice = arr[0:len(arr)] 可以简写 var slice = arr[:]
3 切片的遍历
切片的遍历和数组一样,也有两种方式: for 循环常规方式遍历; for-range 结构遍历切片。
1 func main() { 2 s := []int{1, 3, 5} 3 4 for i := 0; i < len(s); i++ { 5 fmt.Println(i, s[i]) 6 } 7 8 for index, value := range s { 9 fmt.Println(index, value) 10 } 11 }
4 appned()方法为切片添加元素
Go语言的内建函数 appen() 可以为切片动态添加元素,每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在 appen() 函数调用时,所以通常都需要用原变量接收 append() 函数的返回值。
1 func main() { 2 //append()添加元素和切片扩容 3 var numSlice []int 4 for i := 0; i < 10; i++ { 5 numSlice = append(numSlice, i) 6 fmt.Printf("%v len:%d cap:%d ptr:%p\n", numSlice, len(numSlice), cap(numSlice), numSlice) 7 } 8 }
输出:
1 [0] len:1 cap:1 ptr:0xc0000a8000 2 [0 1] len:2 cap:2 ptr:0xc0000a8040 3 [0 1 2] len:3 cap:4 ptr:0xc0000b2020 4 [0 1 2 3] len:4 cap:4 ptr:0xc0000b2020 5 [0 1 2 3 4] len:5 cap:8 ptr:0xc0000b6000 6 [0 1 2 3 4 5] len:6 cap:8 ptr:0xc0000b6000 7 [0 1 2 3 4 5 6] len:7 cap:8 ptr:0xc0000b6000 8 [0 1 2 3 4 5 6 7] len:8 cap:8 ptr:0xc0000b6000 9 [0 1 2 3 4 5 6 7 8] len:9 cap:16 ptr:0xc0000b8000 10 [0 1 2 3 4 5 6 7 8 9] len:10 cap:16 ptr:0xc0000b8000
从上面的结果可以看出:
- append() 函数将元素追加到切片的最后并返回该切片;
- 切片 numSlice 的容量按照1,2,4,8,16这样的规则自动进行扩容,每次扩容后都是扩容前的2倍;
- 切片在动态增加元素时,如果有一次增加的元素超过切片的容量,go底层会抛弃原先的数组,创建一个新的数组newArr,将slice原来包含的元素拷贝到新的数组newArr,slice重新引用到newArr;注意newArr是在底层来维护的,程序员不可见。
4.1 一次追加多个元素
append()函数还支持一次性追加多个元素:
1 var citySlice []string 2 // 追加一个元素 3 citySlice = append(citySlice, "北京") 4 // 追加多个元素 5 citySlice = append(citySlice, "上海", "广州", "深圳") 6 // 追加切片 7 a := []string{"成都", "重庆"} 8 citySlice = append(citySlice, a...) //...代表的含义是将切片a展开,将其中的元素一一添加到citySlice中 9 fmt.Println(citySlice) //[北京 上海 广州 深圳 成都 重庆]
4.2 删除切片中的元素
由于切片没有特定的方法删除切片中的元素,于是使用 append() 函数结合切片本身的特性实现切片中元素的删除。例如:
1 func main() { 2 // 从切片中删除元素 3 a := []int{30, 31, 32, 33, 34, 35, 36, 37} 4 // 要删除索引为2的元素 5 a = append(a[:2], a[3:]...) 6 fmt.Println(a) //[30 31 33 34 35 36 37] 7 }
要从切片 slice 中删除索引为 index 的元素,操作方式是 slice = append(slice[:index], slice[index+1:]...)
5 切片的扩容原则
可以通过查看 $GOROOT/src/runtime/slice.go 源码,其中扩容相关代码如下:
1 newcap := old.cap 2 doublecap := newcap + newcap 3 if cap > doublecap { 4 newcap = cap 5 } else { 6 if old.len < 1024 { 7 newcap = doublecap 8 } else { 9 // Check 0 < newcap to detect overflow 10 // and prevent an infinite loop. 11 for 0 < newcap && newcap < cap { 12 newcap += newcap / 4 13 } 14 // Set newcap to the requested cap when 15 // the newcap calculation overflowed. 16 if newcap <= 0 { 17 newcap = cap 18 } 19 } 20 }
从上面的代码可以看出以下内容:
- 首先判断,如果新申请容量(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 类型的处理方式就不一样。
6 切片的拷贝操作
切片是引用类型,在赋值拷贝和使用 copy() 函数拷贝时要注意二者的区别。
6.1 切片的赋值拷贝
下面的代码中演示了拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容,这点需要特别注意。
1 func main() { 2 s1 := make([]int, 3) //[0 0 0] 3 s2 := s1 //将s1直接赋值给s2,s1和s2共用一个底层数组 4 s2[0] = 100 5 fmt.Println(s1) //[100 0 0] 6 fmt.Println(s2) //[100 0 0] 7 }
切片在函数之间传参时,也遵守引用传递机制,如果在函数中修改切片中的内容,那么也会改变实参。
1 func test(slice []int){ 2 slice[0] = 100 3 } 4 5 func main(){ 6 var slice = []int{1, 2, 3, 4, 5} 7 fmt.Println("slice =", slice) // slice = [1 2 3 4 5] 8 test(slice) 9 fmt.Println("slice =", slice) // slice = [100 2 3 4 5] 10 }
6.2 使用copy()函数复制切片
切片使用copy内置函数完成拷贝,举例说明:
1 func main() { 2 // 切片的拷贝操作 3 var slice1 []int = []int{1, 2, 3, 4, 5} 4 var slice2 = make([]int, 10) 5 copy(slice2, slice1) //将slice1复制到slice2 6 fmt.Println("slice1=", slice1) // slice1= [1 2 3 4 5] 7 fmt.Println("slice2=", slice2) // slice2= [1 2 3 4 5 0 0 0 0 0] 8 }
对上面代码的说明:
- copy(para1, para2)参数的数据类型是切片;
- 按照上面的代码,slice1和slice2的数据空间是独立的,相互不影响,也就是说 slice1[0]=999 , slice2[0] 仍然是1;
注意:如果在拷贝时,目标切片的容量小于源切片的容量,此时目标的容量有多少就存多少源切片的内容:
1 func main() { 2 var a []int = []int{1, 2, 3, 4, 5} 3 var slice = make([]int, 1) 4 fmt.Println(slice) // [0] 5 copy(slice, a) 6 fmt.Println(slice) //[1] 7 }
7 string和slice
- string底层是一个byte数组,因此string也可以进程切片处理,案例演示:
1 func main() { 2 str := "abc" 3 slice := str[1:3] 4 fmt.Printf("%T\n", slice) // string 5 fmt.Println("slice =", slice) // slice = bc 6 fmt.Println(len(slice)) // 2 7 }
- string和切片在内存的形式,以上面代码为例画出内存示意图
- string是不可变得,也就是说不能通过 str[0] = 'z' 方式来修改字符串
- 如果需要修改字符串,可以先将 string -> []byte 或者 []byte -> 修改 -> 重新转成 string ,代码如下:
1 func main() { 2 str1 := "hello world" 3 arr1 := []byte(str1) 4 arr1[0] = 'z' 5 str1 = string(arr1) 6 fmt.Println("str1=", str1) 7 /* 8 细节,我们转成[]byte后,可以处理英文和数字,但是不能处理中文, 9 原因是 []byte 按字节来处理,而一个汉字是3个字节,因此就会出现乱码 10 解决方法是将string转成[]rune即可,因为[]rune是按字符处理,兼容汉字 11 */ 12 str2 := "Hello Golang" 13 arr2 := []rune(str2) 14 arr2[0] = '北' 15 str2 = string(arr2) 16 fmt.Println("str2=", str2) 17 }
8 切片注意事项
切片之间不能比较 ,不能使用==操作符来判断两个切片是否含有全部相等元素。切片唯一合法的比较操作是和 nil 比较。一个 nil 值得切片并没有底层数组,一个 nil 值得切片的长度和容量都是0。但是我们不能说一个长度和容量都是0的切片一定是 nil ,例如下面的示例:
1 var s1 []int //len(s1)=0;cap(s1)=0;s1==nil 2 s2 := []int{} //len(s2)=0;cap(s2)=0;s2!=nil 3 s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil
所以判断一个切片是否是空的,要用 len(s) == 0 来判断,不应该使用 s == nil 来判断。
9 练习题
- 请写出下面代码的输出结果:
1 func main() { 2 var a = make([]string, 5, 10) 3 for i := 0; i < 10; i++ { 4 a = append(a, fmt.Sprintf("%v", i)) 5 } 6 fmt.Println(a) 7 }
- 请使用内置的 sort 包对数组 var a = [...]int{3, 7, 8, 9, 1} 进行排序