slice切片
概述
golang的数组是长度不变的连续内存序列,在实际开发中局限性太高。
slice
切片是由数组封装得到的数据结构,常常被称为可变数组,python中的list
和javascript中的array
实际上都是这种可变数组。
理解
sh
1.数组这种数据结构是在内存中开辟固定连续的空间。
2.切片属于可变数组,也就是说长度可以变化,如果他的长度变化超过了最开始开辟的底层数组的容量,他会重新创建一个容量更大的底层数组。
定义切片
类型定义
基本语法
从切片的来源我们知道,他是个不指定长度,可以动态变化长度的可变数组
,又称动态数组
。
定义语法
go
var 切片名 []类型;
与数组声明的区别就是切片不指定长度。
举例
go
package slice_knowledge
import "fmt"
func DefineSliceByType(){
var slice1 []string
fmt.Printf("切片slice1的零值为%#v\n",slice1)
if(slice1!=nil){
fmt.Println("slice不为nil")
}else{
fmt.Println("slice为nil")
}
}
运行结果
go
切片slice1的零值为[]string(nil)
slice为nil
小结
sh
1.使用类型定义的切片未赋值时为`nil`
注意事项
由于使用类型定义的切片未赋值时为nil
,是没有长度和容量的,所以无法存储任何元素,进行如下操作会报错。
go
package slice_knowledge
import "fmt"
func DefineSliceByType(){
var slice1 []string
fmt.Printf("切片slice1的零值为%#v\n",slice1)
if(slice1!=nil){
fmt.Println("slice不为nil")
}else{
fmt.Println("slice为nil")
}
//尝试赋值
slice1[2] = "hello"
}
报错
sh
panic: runtime error: index out of range [2] with length 0
goroutine 1 [running]:
go_learn/slice_knowledge.DefineSliceByType()
/home/upsilon/workplace/2024_golang/coding/go_learn/slice_knowledge/create.go:16 +0xa9
main.main()
/home/upsilon/workplace/2024_golang/coding/go_learn/main.go:140 +0x4b
exit status 2
我们可以验证当前切片的容量和长度。
go
package slice_knowledge
import (
"fmt"
)
func DefineSliceByType(){
var slice1 []string
fmt.Printf("切片slice1的零值为%#v\n",slice1)
if(slice1!=nil){
fmt.Println("slice1不为nil")
}else{
fmt.Println("slice1为nil")
}
//尝试赋值
// slice1[2] = "hello"
//验证当前容量和长度
fmt.Printf("切片slice1的长度为%v,容量为%v\n",len(slice1),cap(slice1))
}
结果
go
切片slice1的零值为[]string(nil)
slice1为nil
切片slice1的长度为0,容量为0
make定义并初始化
用make
方法定义切片指的是在定义时就给切片开辟出一块空间,这样可以避免上述给nil
赋值的错误。
make
实际上同时进行了定义和初始化的过程。
更推荐使用
make
方法声明切片,因为可读性更高。
基本语法
go
var 切片名 []类型 = make([]类型,切片长度,底层容量)
//左边类型省略
var 切片名 = make([]类型,切片长度,底层容量)
//未指定容量时,golang会在底层做处理,容量>=长度
//所以可以进一步简化
var 切片名 = make([]类型,切片长度)
注意:定义时底层容量必须大于等于切片长度。
举例
go
package slice_knowledge
import (
"fmt"
)
func DefineSliceByMake(){
var slice2 = make([]string,2,3)
fmt.Printf("切片slice2的值为%#v\n",slice2)
//验证当前容量和长度
fmt.Printf("切片slice1的长度为%v,容量为%v\n",len(slice2),cap(slice2))
}
运行结果
sh
切片slice2的值为[]string{"", ""}
切片slice1的长度为2,容量为3
小结
sh
1.make定义并初始化后会用零值填充。
切片初始化
概念解读
sh
1.声明只是注册了标识符
2.初始化是开辟内存空间,用来存储标识的值
赋值初始化
语法
在声明的同时赋值来进行初始化。
go
var 切片名 = []type{元素1,元素2,...}
举例
go
package slice_knowledge
import (
"fmt"
)
func SetValByVal(){
var slice = []int64{1,2,3,4}
fmt.Printf("切片slice的值为%#v\n",slice)
fmt.Printf("切片slice1的长度为%v,容量为%v\n",len(slice),cap(slice))
}
结果
sh
切片slice的值为[]int64{1, 2, 3, 4}
切片slice1的长度为4,容量为4
容量和长度
sh
赋值初始化的切片长度为赋值长度,容量也为赋值的长度。
借助make初始化
make
语法在声明的同时进行初始化,指明了切片的容量与长度。
sh
1.切片的长度
值的实际个数,超过长度就属于越界访问。
没有赋值的元素使用类型零值。
2.切片容量
切片是引用类型,当容量没发生改变时,切片始终引用的是同一底层数组。
举例
go
package slice_knowledge
import (
"fmt"
)
func SetValByMake(){
var slice = make([]int64,5)
fmt.Printf("切片的值为%#v\n",slice)
}
结果
sh
切片的值为[]int64{0, 0, 0, 0, 0}
底层图示
借助数组初始化
基本语法
切片的由来就是他是通过数组的截取得到的数据类型。
语法
go
//前包后不包
var 切片名 = 数组[开始下标:结束下标]
解释
sh
1.这样初始化可以不用指明切片类型,因为可以由数组类型推导
2.得到的切片是由数组前闭后开取得的,即从{开始下标,...,结束下标-1}
3.所得切片长度为结束下标-开始下标,容量为数组长度-开始下标。
举例
go
package slice_knowledge
import (
"fmt"
)
func SetValByArray(){
var array = []int64{1,2,3,4,5,6}
var slice = array[1:3:5]
fmt.Printf("切片的值为%#v\n",slice)
fmt.Printf("切片slice1的长度为%v,容量为%v\n",len(slice),cap(slice))
}
结果
go
切片的值为[]int64{2, 3}
切片slice1的长度为2,容量为5
实际上还有第三个位置,例如
数组[开始下标:结束下标:max]
切片的容量等于
max-1
,注意切片的截取长度
底层图示
各种截取操作
截取语法 | 获取切片 | 切片长度 | 切片容量 |
---|---|---|---|
数组[:] |
从数组索引为0到len(数组长度)-1 处获得切片 |
数组长度 | 数组容量 |
数组[low:] |
从数组索引为low 到len(数组长度)-1 处获得切片 |
数组长度-low |
数组容量-low |
数组[:high] |
从数组索引为0到high 处获得切片 |
high |
数组容量 |
数组[low:high] |
从数组索引为low 到high 处获得切片 |
high-low |
数组容量-low |
数组[low:high:max] |
从数组索引为low 到high 处获得切片 |
high-low |
max-low |
举例
sh
//所有截取的方法
func AllSetValByArray(){
var array = []int64{1,2,3,4,5,6}
//数组[:]
var sliceA = array[:]
fmt.Println("截取方法为数组[:]")
fmt.Printf("切片的值为%#v\n",sliceA)
fmt.Printf("切片sliceA的长度为%v,容量为%v\n",len(sliceA),cap(sliceA))
fmt.Println("*****************************")
var sliceB = array[2:]
fmt.Println("截取方法为数组[2:]")
fmt.Printf("切片的值为%#v\n",sliceB)
fmt.Printf("切片sliceB的长度为%v,容量为%v\n",len(sliceB),cap(sliceB))
fmt.Println("*****************************")
var sliceC = array[:5]
fmt.Println("截取方法为数组[:5]")
fmt.Printf("切片的值为%#v\n",sliceC)
fmt.Printf("切片sliceB的长度为%v,容量为%v\n",len(sliceC),cap(sliceC))
fmt.Println("*****************************")
var sliceD = array[1:3]
fmt.Println("截取方法为数组[1:3]")
fmt.Printf("切片的值为%#v\n",sliceD)
fmt.Printf("切片sliceB的长度为%v,容量为%v\n",len(sliceD),cap(sliceD))
fmt.Println("*****************************")
var sliceE = array[1:3:3]
fmt.Println("截取方法为数组[1:3:3]")
fmt.Printf("切片的值为%#v\n",sliceE)
fmt.Printf("切片sliceB的长度为%v,容量为%v\n",len(sliceE),cap(sliceE))
fmt.Println("*****************************")
}
结果
sh
截取方法为数组[:]
切片的值为[]int64{1, 2, 3, 4, 5, 6}
切片sliceA的长度为6,容量为6
*****************************
截取方法为数组[2:]
切片的值为[]int64{3, 4, 5, 6}
切片sliceB的长度为4,容量为4
*****************************
截取方法为数组[:5]
切片的值为[]int64{1, 2, 3, 4, 5}
切片sliceB的长度为5,容量为6
*****************************
截取方法为数组[1:3]
切片的值为[]int64{2, 3}
切片sliceB的长度为2,容量为5
*****************************
截取方法为数组[1:3:3]
切片的值为[]int64{2, 3}
切片sliceB的长度为2,容量为2
*****************************
注意事项
1.结束下标不得超过开始下标
go
//报错:invalid slice indices: 2 < 3
var sliceF = array[3:2]
//相等是可以的相当于切片长度为0
2.结束下标和开始下标都必须小于数组长度
go
var array = []int64{1,2,3,4,5,6}
/*
报错: panic: runtime error: slice bounds out of range [:8] with capacity 6
*/
var sliceG = array[5:8]
3.max的值要大于等于结束下标
go
//报错:invalid slice indices: 3 < 4
var sliceH = array[1:4:3]
// 切片容量 = max-开始下标
// 切片长度 = 结束下标 - 开始下标
/*
由于 容量>=长度
max-开始下标>= 结束下标 - 开始下标
max >= 结束下标
*/
4.max的值要小于等于数组的容量
go
var array = []int64{1,2,3,4,5,6}
/*
panic: runtime error: slice bounds out of range [::9] with capacity 6
*/
var sliceI = array[1:4:9]
切片的长度和容量
概念讲解
sh
1.切片的长度
当前切片可以存储的元素个数,相当于js声明了一个数组。
访问的下标大于切片长度就会报越界错误。
2.切片的容量
切片的底层大小,切片是动态数组,可以通过append操作来延长切片长度。
当切片的长度大于当前容量时就会触发"扩容"操作。
"扩容"操作后,底层的引用就会改变。
比喻
sh
1.声明切片就好比你买了一座房子(底层数组),"容量"就是房子的大小,"长度"就是你在房子里放的东西个数。
2.我们操作切片,因为都在这座房子里操作,所以相互影响。
3.用append扩充长度,如果房子还装得下,那继续在这个房子中,我们的一切操作都会影响到这个房子。
4.如果房子装不下了,我们就要"扩容",搬到新的更大的房子里,搬家后我们在新房子的操作就与旧房子无关了。
长度和容量方法
go
var sliceX = make([]string,10,15)
//获得切片容量
cap(sliceX)
//获得切片长度
len(sliceX)
公式
go
/*
房子自然要比能放的东西大
*/
切片长度<=切片容量
切片是引用类型
切片截取
我们不仅可以从数组截取得到切片,也可以从切片截取得到新切片。
截取的语法
截取语法 | 获取切片 | 切片长度 | 切片容量 |
---|---|---|---|
切片[:] |
从切片索引为0到len(切片长度)-1 处获得切片 |
切片长度 | 切片容量 |
切片[low:] |
从切片索引为low 到len(切片长度)-1 处获得切片 |
切片长度-low |
切片容量-low |
切片[:high] |
从切片索引为0到high 处获得切片 |
high |
切片容量 |
切片[low:high] |
从切片索引为low 到high 处获得切片 |
high-low |
切片容量-low |
切片[low:high:max] |
从切片索引为low 到high 处获得切片 |
high-low |
max-low |
举例
go
func AllSetValBySlice(){
var slice = make([]int64,6,10)
//数组[:]
var sliceA = slice[:]
fmt.Println("截取方法为数组[:]")
fmt.Printf("切片的值为%#v\n",sliceA)
fmt.Printf("切片sliceA的长度为%v,容量为%v\n",len(sliceA),cap(sliceA))
fmt.Println("*****************************")
var sliceB = slice[2:]
fmt.Println("截取方法为数组[2:]")
fmt.Printf("切片的值为%#v\n",sliceB)
fmt.Printf("切片sliceB的长度为%v,容量为%v\n",len(sliceB),cap(sliceB))
fmt.Println("*****************************")
var sliceC = slice[:5]
fmt.Println("截取方法为数组[:5]")
fmt.Printf("切片的值为%#v\n",sliceC)
fmt.Printf("切片sliceB的长度为%v,容量为%v\n",len(sliceC),cap(sliceC))
fmt.Println("*****************************")
var sliceD = slice[1:3]
fmt.Println("截取方法为数组[1:3]")
fmt.Printf("切片的值为%#v\n",sliceD)
fmt.Printf("切片sliceB的长度为%v,容量为%v\n",len(sliceD),cap(sliceD))
fmt.Println("*****************************")
var sliceE = slice[1:3:3]
fmt.Println("截取方法为数组[1:3:3]")
fmt.Printf("切片的值为%#v\n",sliceE)
fmt.Printf("切片sliceB的长度为%v,容量为%v\n",len(sliceE),cap(sliceE))
fmt.Println("*****************************")
}
结果
sh
截取方法为数组[:]
切片的值为[]int64{0, 0, 0, 0, 0, 0}
切片sliceA的长度为6,容量为10
*****************************
截取方法为数组[2:]
切片的值为[]int64{0, 0, 0, 0}
切片sliceB的长度为4,容量为8
*****************************
截取方法为数组[:5]
切片的值为[]int64{0, 0, 0, 0, 0}
切片sliceB的长度为5,容量为10
*****************************
截取方法为数组[1:3]
切片的值为[]int64{0, 0}
切片sliceB的长度为2,容量为9
*****************************
截取方法为数组[1:3:3]
切片的值为[]int64{0, 0}
切片sliceB的长度为2,容量为2
*****************************
同底切片互相影响
何谓同底层切片
sh
1.从同一个数组截取的切片
2.从同一切片截取到的切片及其衍生切片
3.赋值得到的切片(因为切片是引用类型)
验证
go
package slice_knowledge
import (
"fmt"
)
func VerifySlice(){
var array = [5]int64{1,2,3,4,5}
fmt.Printf("sliceA增加前,array的值为%#v\n",array)
var sliceA = array[:2]
fmt.Printf("sliceA增加前,sliceA的值为%#v\n",sliceA)
var sliceB = array[2:4]
fmt.Printf("sliceA增加前,sliceB的值为%#v\n",sliceB)
sliceA = append(sliceA, 8)
fmt.Printf("sliceA增加后,array的值为%#v\n",array)
fmt.Printf("sliceA增加后,sliceA的值为%#v\n",sliceA)
fmt.Printf("sliceA增加后,sliceB的值为%#v\n",sliceB)
}
结果
sh
sliceA增加前,array的值为[5]int64{1, 2, 3, 4, 5}
sliceA增加前,sliceA的值为[]int64{1, 2}
sliceA增加前,sliceB的值为[]int64{3, 4}
sliceA增加后,array的值为[5]int64{1, 2, 8, 4, 5}
sliceA增加后,sliceA的值为[]int64{1, 2, 8}
sliceA增加后,sliceB的值为[]int64{8, 4}
切片方法
Append增加切片元素
源码
go
// The append built-in function appends elements to the end of a slice. If
// it has sufficient capacity, the destination is resliced to accommodate the
// new elements. If it does not, a new underlying array will be allocated.
// Append returns the updated slice. It is therefore necessary to store the
// result of append, often in the variable holding the slice itself:
//
// slice = append(slice, elem1, elem2)
// slice = append(slice, anotherSlice...)
//
// As a special case, it is legal to append a string to a byte slice, like this:
//
// slice = append([]byte("hello "), "world"...)
func append(slice []Type, elems ...Type) []Type
基本语法
go
//在切片末尾添加1个元素
append(原始切片,元素1)
//在切片末尾依次添加多个元素
append(原始切片,元素1,元素2,...)
//在切片末尾添加其他切片的所有元素
append(原始切片,其他切片...)
返回值
sh
1.一个切片
如果触发扩容(原始切片长度+元素个数>原始切片容量),则返回的切片与原始切片无关
如果没有触发扩容,则返回的切片与原始切片共享底层
举例
go
package slice_knowledge
import (
"fmt"
)
func AppendMethod(){
var sliceA = make([]int64,5,8)
fmt.Printf("append操作前,sliceA的值为%#v\n",sliceA)
var array =[]int64{9,99,999,9999}
var sliceB = array[:]
fmt.Printf("append操作前,sliceB的值为%#v\n",sliceB)
//append添加一个元素
sliceC := append(sliceA,8)
fmt.Printf("sliceC的值为%#v\n",sliceC)
//append方法不会改变原始切片的长度
fmt.Printf("append操作后,sliceA的值为%#v\n",sliceA)
//没有触发扩容时原始切片和结果切片共用底层
sliceC[1] = 55
fmt.Printf("sliceC修改后的值为%#v\n",sliceC)
fmt.Printf("sliceC修改后,sliceA的值为%#v\n",sliceA)
//添加多个元素(仍然未触发扩容时)
sliceD := append(sliceA,5,4)
fmt.Printf("sliceD的值为%#v\n",sliceD)
//原因是sliceA、sliceC、sliceD共用底层,所以sliceD的操作影响到了sliceC
fmt.Printf("sliceC的值为%#v\n",sliceC)
fmt.Printf("append操作后,sliceA的值为%#v\n",sliceA)
//拓展运算符合
sliceE :=append(sliceA,sliceB...)
fmt.Printf("sliceE的值为%#v\n",sliceD)
fmt.Printf("sliceE修改前,sliceA的值为%#v\n",sliceA)
//最终长度>sliceA的容量,扩容了,sliceE与sliceA无关了
sliceE[0] = 7878
fmt.Printf("sliceE修改后,sliceA的值为%#v\n",sliceA)
}
结果
sh
append操作前,sliceA的值为[]int64{0, 0, 0, 0, 0}
append操作前,sliceB的值为[]int64{9, 99, 999, 9999}
sliceC的值为[]int64{0, 0, 0, 0, 0, 8}
append操作后,sliceA的值为[]int64{0, 0, 0, 0, 0}
sliceC修改后的值为[]int64{0, 55, 0, 0, 0, 8}
sliceC修改后,sliceA的值为[]int64{0, 55, 0, 0, 0}
sliceD的值为[]int64{0, 55, 0, 0, 0, 5, 4}
sliceC的值为[]int64{0, 55, 0, 0, 0, 5}
append操作后,sliceA的值为[]int64{0, 55, 0, 0, 0}
sliceE的值为[]int64{0, 55, 0, 0, 0, 5, 4}
sliceE修改前,sliceA的值为[]int64{0, 55, 0, 0, 0}
sliceE修改后,sliceA的值为[]int64{0, 55, 0, 0, 0}
案例解读
go
func AppendMethod(){
var sliceA = make([]int64,5,8)
fmt.Printf("append操作前,sliceA的值为%#v\n",sliceA)
sliceC := append(sliceA,88)
//添加多个元素(仍然未触发扩容时)
sliceD := append(sliceA,5,4)
fmt.Printf("sliceD的值为%#v\n",sliceD)
//原因是sliceA、sliceC、sliceD共用底层,所以sliceD的操作影响到了sliceC
fmt.Printf("sliceC的值为%#v\n",sliceC)
fmt.Printf("append操作后,sliceA的值为%#v\n",sliceA)
}
解释
sh
1.sliceA的容量是8
2.sliceC是sliceA增加一个元素得到的,(长度=6)<容量8,所以共享底层。
可以拆分为两步:
先设立一个新变量sliceC,长度为len(sliceA)+1
然后赋值 sliceC[6] = 88
底层 slice[6] = 88
3.sliceD是sliceA增加两个元素得到的,(长度=5+2)<容量8,所以共享底层
可以拆分为两步:
先设立一个新变量sliceD,长度为len(sliceA)+2
然后赋值sliceD[6]=5,sliceD[7]=4
底层slice[6]=5,slice[7]=4
4.由于sliceA、sliceC、sliceD共享底层,所以sliceC也受到了影响
可以理解为所有共享同底层的切片都是底层切片的投影。
copy方法
基本语法
源码
go
// The copy built-in function copies elements from a source slice into a
// destination slice. (As a special case, it also will copy bytes from a
// string to a slice of bytes.) The source and destination may overlap. Copy
// returns the number of elements copied, which will be the minimum of
// len(src) and len(dst).
func copy(dst, src []Type) int
解释
函数 copy 在两个 slice 间复制数据,复制长度以 len 小的为准。两个 slice 可指向同一底层数组,允许元素区间重叠。
语法
go
copy(切片A,切片B)
返回值
sh
更改元素的个数
举例
go
func CopyMethod(){
var sliceA = []int64{1,2,3}
var sliceB = []int64{11,22,33,44,55,66,77,88}
fmt.Printf("copy前sliceA的值为%#v\n",sliceA)
fmt.Printf("copy前sliceB的值为%#v\n",sliceB)
copy(sliceA,sliceB)
fmt.Printf("copy后sliceA的值为%#v\n",sliceA)
fmt.Printf("copy后sliceB的值为%#v\n",sliceB)
var sliceC = []int64{1,2,3}
var sliceD = []int64{11,22,33,44,55,66,77,88}
fmt.Printf("copy前sliceC的值为%#v\n",sliceC)
fmt.Printf("copy前sliceD的值为%#v\n",sliceD)
x:=copy(sliceD,sliceC)
fmt.Printf("copy后sliceC的值为%#v\n",sliceC)
fmt.Printf("copy后sliceD的值为%#v\n",sliceD)
fmt.Printf("更改元素的个数为%d\n",x)
}
结果
sh
copy前sliceA的值为[]int64{1, 2, 3}
copy前sliceB的值为[]int64{11, 22, 33, 44, 55, 66, 77, 88}
copy后sliceA的值为[]int64{11, 22, 33}
copy后sliceB的值为[]int64{11, 22, 33, 44, 55, 66, 77, 88}
copy前sliceC的值为[]int64{1, 2, 3}
copy前sliceD的值为[]int64{11, 22, 33, 44, 55, 66, 77, 88}
copy后sliceC的值为[]int64{1, 2, 3}
copy后sliceD的值为[]int64{1, 2, 3, 44, 55, 66, 77, 88}
更改元素的个数为3
解释
sh
1.copy(A,B)
将B的值赋给A,A与B的长度不改变只改变值,如果有对应位置的值就赋值,否则保留之前的值
2.如果A的长度4>B的长度3
A[0] = B[0]
A[1]=B[1]
A[2]=B[2]
A[3]=A[3]
...
3.如果A的长度3<B的长度4
A[0]=B[0]
A[1]=B[1]
A[2]=B[2]
对于共用底层的切片
go
func CopyMethod(){
var array = []int64{1,2,3,4,5,6}
sliceE := array[:]
sliceF := array[2:4]
fmt.Printf("copy前sliceE的值为%#v\n",sliceE)
fmt.Printf("copy前sliceF的值为%#v\n",sliceF)
copy(sliceF,sliceE)
fmt.Printf("copy后sliceE的值为%#v\n",sliceE)
fmt.Printf("copy后sliceF的值为%#v\n",sliceF)
}
结果
sh
copy前sliceE的值为[]int64{1, 2, 3, 4, 5, 6}
copy前sliceF的值为[]int64{3, 4}
copy后sliceE的值为[]int64{1, 2, 1, 2, 5, 6}
copy后sliceF的值为[]int64{1, 2}
解释
go
copy(sliceF,sliceE)
将sliceE赋值给sliceF,
sliceE[0] = sliceF[0] => 1
由于共底,底层 silce[2] = 1
(因为sliceE[0]是底层slice[2],则sliceF变成了[1,2,1,4,5,6])
sliceE[1]=sliceF[1]=>2
由于共底,底层 silce[3] = 2
(因为sliceE[1]是底层slice[3],则sliceF变成了[1,2,1,2,5,6])