飞道的博客

玩好go的切片

375人阅读  评论(0)

go的slice ,入门就会遇到,但这个东西大多数人都是停留在简单的使用。一些干了好几年的老程序员都说不明白里面的道道,这里面坑不少。恰巧今天有空,好好整理下,永不踩坑

为什么要用切片

其他语言大多用的都是数组,在go中,数组的长度是不可变化的,声明后就是固定的,所以go有了切片,长度是可变化的我们平时用的最多的也是切片。

什么是切片

基本概念

  1. slice
  2. 切片是数组的引用类型,故是引用传递(其实是值传递,下面会细细分析)
  3. 切片的使用和数组类似
  4. 切片的长度是可以变化的(动态变化的数组)
  5. 切片的定义语法:
    var 切片名 []数据类型(没有长度
    如 var laozhao []int

切片在内存中得布局

  1. 切片是数组的引用,所以出现顺序数组肯定是先于切片的(可以理解为切片是数组上的一个滑动窗口)
  2. 声明一个数组 intArr,首先在内存中开辟一个数组空间,空间中存放我们声明的数(这里值得是数组中22的地址)
  3. 然后我们声明了一个切片,切片是数组下标从第1开始到3之前的部分(包头不包尾)。此时在内存中会开辟另一块空间,切片的本质是一个结构体,一共三部分:切片中首元素在数组中得地址、长度、容量
  4. 改变切片中得数据会改变数组的值

切片数据结构本质是一个结构体
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
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场