go的slice ,入门就会遇到,但这个东西大多数人都是停留在简单的使用。一些干了好几年的老程序员都说不明白里面的道道,这里面坑不少。恰巧今天有空,好好整理下,永不踩坑
为什么要用切片
其他语言大多用的都是数组,在go中,数组的长度是不可变化的,声明后就是固定的,所以go有了切片,长度是可变化的我们平时用的最多的也是切片。
什么是切片
基本概念
- slice
- 切片是数组的引用类型,故是引用传递(其实是值传递,下面会细细分析)
- 切片的使用和数组类似
- 切片的长度是可以变化的(动态变化的数组)
- 切片的定义语法:
var 切片名 []数据类型(没有长度)
如 var laozhao []int
切片在内存中得布局
- 切片是数组的引用,所以出现顺序数组肯定是先于切片的(可以理解为切片是数组上的一个滑动窗口)
- 声明一个数组 intArr,首先在内存中开辟一个数组空间,空间中存放我们声明的数(这里值得是数组中22的地址)
- 然后我们声明了一个切片,切片是数组下标从第1开始到3之前的部分(包头不包尾)。此时在内存中会开辟另一块空间,切片的本质是一个结构体,一共三部分:切片中首元素在数组中得地址、长度、容量
- 改变切片中得数据会改变数组的值
切片数据结构本质是一个结构体
type slice struct{
ptr unsafe.Pointer
len int
cap int
}
值传递和引用传递
在了解切片的传递模式之前先了解下什么是值传递和引用传递
值传递:方法调用时,实际参数把它的值传递给对应的形式参数,方法执行中形式参数值的改变不影响实际参数的值。
引用传递:也称为传地址。方法调用时,实际参数的引用(地址,而不是参数的值)被传递给方法中相对应的形式参数,在方法执行中,对形式参数的操作实际上就是对实际参数的操作,方法执行中形式参数值的改变将会影响实际参数的值。
切片是值传递还是引用传递
首先看这段代码的运行结果
func main() {
var a = [3]int{
6, 6, 6}
ages := a[:]
//fmt.Printf("%T\n", ages)
//fmt.Printf("%p\n",&ages)
//fmt.Printf("数组第一个元素的地址%v\n", &a[0])
//fmt.Printf("切片第一个元素的地址%v\n", &ages[0])
fmt.Printf("原始slice的内存地址是%p\n", ages)
modify(ages)
fmt.Println(ages)
}
func modify(ages []int) {
fmt.Printf("函数里接收到slice的内存地址是%p\n", ages)
ages[0] = 1
}
运行结果
这里看到的,初始切片的地址是 0xc0000a0120,传入 modify 函数后的地址还是 0xc0000a0120,并且在 modify 中对 切片的操作是可以影响到初始的切片的,
所以很多人就会说 切片是引用传递,其实不然,我们看下面代码
func main() {
var a = [3]int{
6, 6, 6}
ages := a[:]
//fmt.Printf("%T\n", ages)
//fmt.Printf("%p\n",&ages)
fmt.Printf("数组第一个元素的地址%v\n", &a[0])
fmt.Printf("切片第一个元素的地址%v\n", &ages[0])
fmt.Printf("原始slice的内存地址是%p\n", ages)
modify(ages)
fmt.Println(ages)
}
func modify(ages []int) {
fmt.Printf("函数里接收到slice的内存地址是%p\n", ages)
ages[0] = 1
}
运行结果
数组首地址、切片首地址、切片地址 都是一样的
恍然大悟,原来 %p 打印出来的时 切片中存入的数组的首地址,在函数传参时也传的是 地址的一个拷贝
为什么%p打印的是切片中指向数组的地址,是因为 fmt 内部对切片有特殊的处理
那么切片自身也是有地址的,就是下图红框圈出来的部分
看下切片自身的地址是什么
func main() {
var a = [3]int{
6, 6, 6}
ages := a[:]
//fmt.Printf("%T\n", ages)
fmt.Printf("切片自身的地址 %p\n",&ages)
//fmt.Printf("数组第一个元素的地址%v\n", &a[0])
//fmt.Printf("切片第一个元素的地址%v\n", &ages[0])
fmt.Printf("原始slice的内存地址是%p\n", ages)
modify(ages)
fmt.Println(ages)
}
func modify(ages []int) {
fmt.Printf("函数里接收到slice的内存地址是%p\n", ages)
fmt.Printf("切片自身的地址 %p\n",&ages)
ages[0] = 1
}
运行结果
可以看到切片自身的地址在作为参数传入后是发生变化的,是值传递。之所以在函数中的操作能改变原切片中得值,是因为切片中存的数组首地址是相同的
当然,作为参数传入后,是无法修改原切片的len和cap的,如果len和cap发生了变化,指向的数组将发生变化,看下面这个代码
func main() {
var a = [3]int{
6, 6, 6}
ages := a[:]
//fmt.Printf("%T\n", ages)
//fmt.Printf("切片自身的地址 %p\n", &ages)
//fmt.Printf("数组第一个元素的地址%v\n", &a[0])
//fmt.Printf("切片第一个元素的地址%v\n", &ages[0])
fmt.Printf("原始slice的内存地址是%p\n", ages)
modify(ages)
fmt.Println(ages)
}
func modify(ages []int) {
fmt.Printf("函数里接收到slice的内存地址是%p\n", ages)
//fmt.Printf("切片自身的地址 %p\n", &ages)
fmt.Println(len(ages)," ", cap(ages))
ages = append(ages, 6)
fmt.Println(len(ages)," ", cap(ages))
fmt.Printf("函数里接收到slice的内存地址是%p\n", ages)
//fmt.Printf("切片自身的地址 %p\n", &ages)
}
运行结果
可以看到切片中得数组首地址发生了变化,原来的切片数据也没发生变化。
如果想修改原切片的的cap(如append操作),则需要传入指针,看下面演示
func main() {
var a = [3]int{
6, 6, 6}
ages := a[:]
//fmt.Printf("%T\n", ages)
//fmt.Printf("切片自身的地址 %p\n", &ages)
//fmt.Printf("数组第一个元素的地址%v\n", &a[0])
//fmt.Printf("切片第一个元素的地址%v\n", &ages[0])
fmt.Printf("原始slice的内存地址是%p\n", ages)
modify(&ages) // 传入的是指针
fmt.Println(ages)
}
func modify(ages *[]int) {
fmt.Printf("函数里接收到slice的内存地址是%p\n", ages)
//fmt.Printf("切片自身的地址 %p\n", &ages)
fmt.Println(len(*ages)," ", cap(*ages))
*ages = append(*ages, 6)
fmt.Println(len(*ages)," ", cap(*ages))
fmt.Printf("函数里接收到slice的内存地址是%p\n", ages)
//fmt.Printf("切片自身的地址 %p\n", &ages)
}
可以看到通过传入切片指针,可以改变原切片的cap。原切片指向了新的数组
切片 appent 操作 相关的坑
append的时候要关注cap是否更改,如果cap更新的话,底层会指向新的数组(扩容后新建的数组)
func test(){
var array =[]int{
1,2,3,4,5}// len:5,capacity:5
var newArray=array[1:3]// len:2,capacity:4 (已经使用了两个位置,所以还空两位置可以append)
fmt.Printf("%p\n",array) //0xc420098000
fmt.Printf("%p\n",newArray) //0xc420098008 可以看到newArray的地址指向的是array[1]的地址,即他们底层使用的还是一个数组
fmt.Printf("%v\n",array) //[1 2 3 4 5]
fmt.Printf("%v\n",newArray) //[2 3]
newArray[1]=9 //更改后array、newArray都改变了
fmt.Printf("%v\n",array) // [1 2 9 4 5]
fmt.Printf("%v\n",newArray) // [2 9]
newArray=append(newArray,11,12)//append 操作之后,array的len和capacity不变,newArray的len变为4,capacity:4。因为这是对newArray的操作
fmt.Printf("%v\n",array) //[1 2 9 11 12] //注意对newArray做append操作之后,array[3],array[4]的值也发生了改变
fmt.Printf("%v\n",newArray) //[2 9 11 12]
newArray=append(newArray,13,14) // 因为newArray的len已经等于capacity,所以再次append就会超过capacity值,
// 此时,append函数内部会创建一个新的底层数组(是一个扩容过的数组),并将array指向的底层数组拷贝过去,然后在追加新的值。
fmt.Printf("%p\n",array) //0xc420098000
fmt.Printf("%p\n",newArray) //0xc4200a0000
fmt.Printf("%v\n",array) //[1 2 9 11 12]
fmt.Printf("%v\n",newArray) //[2 9 11 12 13 14] 他两已经不再是指向同一个底层数组y了
}
切片的复制
切片复制可以用copy也可以用等号
copy是引用复制,复制的时切片内指向数组的指针及相关信息
等号是完全进行的值拷贝,会在内存中开辟新的空间,建立新的数组
func main() {
sl_from := []int{
1, 2, 3}
b := make([]int, len(sl_from))
copy(b, sl_from)
a := sl_from
fmt.Printf("原切片:%p copy后的:%p 等于后的:%p\n", sl_from, b, a)
fmt.Printf("原切片:%p copy后的:%p 等于后的:%p\n", &sl_from, &b, &a)
}
根据结果可以看出,copy过的两个切片的地址是完全不同的
但是等于后的切片,指向的数组还是同一个
转载:https://blog.csdn.net/weixin_43753680/article/details/114117491