1 为什么要引入切片?
go数组的缺陷:声明时确定容量,无法自动扩容;值传递,无法处理引用传递的数据;
我们的需求:
- 不需要声明容量,并且自动扩容,支持增删等操作的一个数据结构
- 其他语言里就有这样的东西,比如Python里的List,C++里的Vector,JAVA里的ArrayList。
2 切片的基本介绍
切片需要包括三个变量:
- 开头指针,指向了切片开头的地址
- len,标明了使用到的空间长度
- cap,表示总空间长度
切片操作主要包括
- 增,见下文切片新增元素
- 删,go没有实现这个操作,需要通过重复切片来实现
- 查改,涉及切片的遍历,切片的遍历与数组的遍历一样,都可以使用for或for range遍历
3 切片的定义与初始化
3.1 直接声明和初始化
定义1——var
var identifier []type
:T为变量类型,切片不需要说明长度,可以在声明时直接赋值,如果不赋值,则该切片len及capacity均为0,且只能通过append函数追加数据
var a []int //只声明,未分配空间,不能通过下标方式使用,此时,len=cap=0
切片定义完后,还不能使用,因为本身是一个空的,需要让其引用到一个数组,或者 make 一个空间供切片来使用
定义2—— := 定义与初始化
- 直接初始化切片 :
s :=[] int {1,2,3}
[] 表示是切片类型,{1,2,3} 初始化值依次是 1,2,3,其 cap=len=3
定义3——切片截取,通过已有数组创建
可以通过设置下限(startIndex)及上限(endIndex)来设置截取切片 [lower-bound:upper-bound] ,
说明:从 arr 数组下标为 startIndex,取到 下标为 endIndex 的元素(不含 arr[endIndex])。
-
初始化切片 s,是数组 arr 的引用。
s := arr[:]
等价于var slice = arr[0:len(arr)
-
将 arr 中从下标 startIndex 到 endIndex-1 下的元素创建为一个新的切片。
s := arr[startIndex:endIndex]
s := arr[:]
与s := arr[startIndex:endIndex]
两者等效
- 默认 startIndex 时将表示从 arr 的第一个元素开始。
s := arr[:endIndex]
等价于var slice = arr[0:end]
- 默认 endIndex 时将表示一直到arr的最后一个元素。
s := arr[startIndex:]
等价于var slice = arr[start:len(arr)]
a := [6]int{12,43,23,56,75,66}
//a是个长度为6的数组,a[0]=12
b := a[1:3:5]
从a的下标1切到3,且不包含3,最大容量到下标5,此时,b=[43 23],len=2,cap=4,此时,与凭空创建不同的是,此时b没有用到的两个位置(cap-len=2)是有元素的,他们是56和75
b[3] = 111
//将会报错,因为b[3]虽然有东西,但len=2,只能访问b[0]和b[1]
b = b[0:4:4]
//在b上重复切片,屁股往后挪,b=[43 23 56 75],len=4,cap=4
b[3] = 111
//不会报错了
实例如下:
package main
import "fmt"
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
func main() {
/* 创建切片 */
numbers := []int{0,1,2,3,4,5,6,7,8}
printSlice(numbers)
//len=9 cap=9 slice=[0 1 2 3 4 5 6 7 8]
/* 打印原始切片 */
fmt.Println("numbers ==", numbers)
//numbers == [0 1 2 3 4 5 6 7 8]
/* 打印子切片从索引1(包含) 到索引4(不包含)*/
fmt.Println("numbers[1:4] ==", numbers[1:4])
//numbers[1:4] == [1 2 3]
/* 默认startIndex为 0*/
fmt.Println("numbers[:3] ==", numbers[:3])
//numbers[:3] == [0 1 2]
/* 默认endIndex为 len(s)-1*/
fmt.Println("numbers[4:] ==", numbers[4:])
//numbers[4:] == [4 5 6 7 8]
numbers1 := make([]int,0,5)
printSlice(numbers1)
//len=0 cap=5 slice=[]
/* 打印子切片从索引 0(包含) 到索引 2(不包含) */
number2 := numbers[:2]
printSlice(number2)
//len=2 cap=9 slice=[0 1]
/* 打印子切片从索引 2(包含) 到索引 5(不包含) */
number3 := numbers[2:5]
printSlice(number3)
//len=3 cap=7 slice=[2 3 4]
}
3.2 通过 make() 函数来创建切片
slice1 := make([]T, length, capacity)
var slice1 []type = make([]type)
//不指定容量和长度
var b = make([]int) //此时,len=cap=0
slice1 := make([]type, lenth)
//不指定容量
var b = make([]int, 3) //此时,len=cap=3,全初始化为0
slice1 := make([]T, length, capacity)
// 指定长度与容量
var a = make([]int,3,5) //此时,len=3,cap=5,且a的前三个元素自动初始化为0
通过内置函数 make() 初始化切片slice1,[]int 标识为其元素类型为 int 的切片。
slice2 := make([]int,len,cap)
通过切片 s 初始化切片 s1。s := make([]int,len,cap)
通过内置函数 make() 初始化切片s,[]int 标识为其元素类型为 int 的切片。s :=make([]int,len,cap)
3.3 初始化常见错误
初始化常见错误1
var a []int //只声明,未分配空间,不能通过下标方式使用
a[0] = 1 //将报错
fmt.Println(nil == a) //将输出true,说明只声明时,a相当于nil
在Go语言中,声明一个切片(slice)时,需要使用make()函数或者字面量语法进行初始化,否则切片的底层数组为nil,无法进行任何操作,因此会报错。
对于你以上的代码,a是一个切片,但是没有初始化。因此,当你尝试给a中的第一个元素a[0]赋值时,Go语言会返回一个“Index out of range”的错误,因为a的长度为0,没有任何元素。
可以a := make([]int, 1)
a[0] = 1
或者使用字面量语法进行初始化:a := []int{1}
这样就可以给a中的第一个元素赋值了。
初始化常见错误2
var b = []int{} //声明并分配空间,len=cap=0
b[0] = 1 //将报错
在Go语言中,使用字面量语法声明一个空切片时,切片的底层数组被分配了内存空间,但是长度为0。因此,当你尝试给空切片b中的第一个元素b[0]赋值时,Go语言会返回一个“Index out of range”的错误,因为b没有任何元素,长度为0,无法访问下标为0的元素。
如果你需要在空切片中添加元素,可以使用append()函数。例如:b := []int{}
b = append(b, 1)
4 切片的赋值(增)–append()与copy()
4.1 len() 和 cap() 函数
切片是可索引的,并且可以由 len() 方法获取长度。
切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。
package main
import "fmt"
func main() {
var numbers = make([]int,3,5)
printSlice(numbers)
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
len=3 cap=5 slice=[0 0 0]
4.2 空(nil)切片
一个切片在未初始化之前默认为 nil,长度为 0,实例如下:
package main
import "fmt"
func main() {
var numbers []int
printSlice(numbers)
if(numbers == nil){
fmt.Printf("切片是空的")
}
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
len=0 cap=0 slice=[]
切片是空的
4.3 append()
如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。
下面的代码描述了从拷贝切片的 copy 方法和向切片追加新元素的 append 方法。
package main
import "fmt"
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
func main() {
var numbers []int
printSlice(numbers)
//len=0 cap=0 slice=[]
/* 允许追加空切片 */
numbers = append(numbers, 0)
printSlice(numbers)
//len=1 cap=1 slice=[0]
/* 向切片添加一个元素 */
numbers = append(numbers, 1)
printSlice(numbers)
//len=2 cap=2 slice=[0 1]
/* 同时添加多个元素 */
numbers = append(numbers, 2,3,4)
printSlice(numbers)
//len=5 cap=6 slice=[0 1 2 3 4]
var s []int{100}
ss := append(s,numbers...) //append的另一种用法,将C解开后,添加到a中
/* 创建切片 numbers1 是之前切片的两倍容量*/
numbers1 := make([]int, len(numbers), (cap(numbers))*2)
/* 拷贝 numbers 的内容到 numbers1 */
copy(numbers1,numbers)
printSlice(numbers1)
//len=5 cap=12 slice=[0 1 2 3 4]
}
如果新增后cap装不下了,就必须自动扩容;
4.4 切片的自动扩容 & 切片在内存中的形式
切片 append 操作的底层原理分析:
切片 append 操作的本质就是对数组扩容
go 底层会创建一下新的数组 newArr(安装扩容后大小)
将 slice 原来包含的元素拷贝到新的数组 newArr
slice 重新引用到 newArr
注意 newArr 是在底层来维护的,程序员不可见.
- 当容量到达原来的上限时,直接把原来的容量翻倍,重新申请一块内存,并把数据复制过去。(注意,此时内存位置发生了变化!!)
- 如果一次添加的很多,翻倍也不够怎么办?先翻倍,然后一直+2,直到符合要求
- cap已经很大了,比如1024,再翻倍要很久才能填满,浪费空间。那就不翻倍,一次添加原来的256(即1024的1/4),这种说法在一定范围内是正确的
https://studygolang.com/articles/34462
5 删
var a = []string{"A","B","C","D","E"}
a = append(a[:2],a[3:]...)
fmt.Print(a) //输出[A B D E]
//要删除下标为x的元素,则
x := 2
a=append(a[:x],a[x+1:]...)
关于...
//三个点可以用在:
var a = […]int{1,2} //自动推导数组长度
append(a,b…) //解开切片
func append(slice []Type, elems …Type) []Type //接受不定长参数
6 复制
/* 创建切片 numbers1 是之前切片的两倍容量*/
numbers1 := make([]int, len(numbers), (cap(numbers))*2)
/* 拷贝 numbers 的内容到 numbers1 */
copy(numbers1,numbers)
printSlice(numbers1)
//len=5 cap=12 slice=[0 1 2 3 4]
同样,简单赋值时,复制的也是指针:
var a = []int{9,99,999}
b := a
b[0] = 1
fmt.Print(a) //a变成了[1 99 999]
但有时,我们不想让复制后的指针修改原来的值,此时我们需要使用到copy函数,将原切片底层数组的值复制到新的地方,之后的操作就在新的地方进行了:
var a = []int{9,99,999}
b := make([]int,3)
copy(b,a)
b[0] = 1
fmt.Print(a) //a还是[9 99 999]
7 切片传参(改)
之前我们学到过,数组作为参数传递给函数,传递参数的形式是值传递,也就是说函数内部会自己开一块空间,复制这个传进来的数组,之后的操作都在自己的空间进行,不会修改原来的数组:
func change(a [3]int){
a[0] = 1
}
func main() {
var a = [...]int{9,99,999}
change(a)
fmt.Print(a) //a还是[9 99 999]
}
但我们使用切片,就可以改变原始的内容,因为切片里存的是指针,传过去的时候,函数内部也会自己开一块空间,复制这个传进来的切片,但复制前后,切片内部存的指针指向的是同一块地址,因此函数内部使用自己复制出来的切片进行操作时,也会影响到原始的切片
func change(a []int){
a[0] = 1
}
func main() {
var a = []int{9,99,999}
change(a)
fmt.Print(a) //a变成了[1 99 999]
}
8 切片的遍历
切片的遍历
切片的遍历和数组一样,也有两种方式
-
for 循环常规方式遍历
-
for-range 结构遍历切片
string与slice
- string 底层是一个 byte 数组,因此 string 也可以进行切片处理 案例演示
str := "hello@atguigu"
//使用切片获取到 atguigu
slice := str[6:]
fmt.Println("slice=", slice)
-
tring 和切片在内存的形式,以 “abcd” 画出内存示意图
-
string 是不可变的,也就说不能通过 str[0] = ‘z’ 方式来修改字符串
string是不可变的,也就说不能通过 str[0] = 'z' 方式来修改字符串
str[0] = 'z' [编译不会通过,报错,原因是string是不可变]
- 如果需要修改字符串,可以先将 string -> []byte / 或者 []rune -> 修改 -> 重写转成 string
//如果需要修改字符串,可以先将string -> []byte / 或者 []rune -> 修改 -> 重写转成string
// "hello@atguigu" =>改成 "zello@atguigu"
arr1 := []byte(str)
arr1[0] = 'z'
str = string(arr1)
fmt.Println("str=", str)
- 我们转成[]byte后,可以处理英文和数字,但是不能处理中文
// 原因是 []byte 字节来处理 ,而一个汉字,是3个字节,因此就会出现乱码
// 解决方法是 将 string 转成 []rune 即可, 因为 []rune是按字符处理,兼容汉字
arr1 := []rune(str)
arr1[0] = '北'
str = string(arr1)
fmt.Println("str=", str)
参考
【1】https://studygolang.com/articles/34462
【2】尚硅谷
【3】https://www.runoob.com/go/go-slice.html