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