go的slice ,入门就会遇到,但这个东西大多数人都是停留在简单的使用。一些干了好几年的老程序员都说不明白里面的道道,这里面坑不少。恰巧今天有空,好好整理下,永不踩坑
1、为什么要用切片
其他语言大多用的都是数组,在go中,数组的长度是不可变化的,声明后就是固定的,所以go有了切片,长度是可变化的我们平时用的最多的也是切片。
2、什么是切片
2.1基本概念
- slice
- 切片是数组的引用类型,故是引用传递(其实是值传递,下面会细细分析)
- 切片的使用和数组类似
- 切片的长度是可以变化的(动态变化的数组)
- 切片的定义语法:
var 切片名 []数据类型(没有长度)
如 var laozhao []int
3、切片在内存中得布局
- 切片是数组的引用,所以出现顺序数组肯定是先于切片的(可以理解为切片是数组上的一个滑动窗口)
- 声明一个数组 intArr,首先在内存中开辟一个数组空间,空间中存放我们声明的数(这里值得是数组中22的地址)
- 然后我们声明了一个切片,切片是数组下标从第1开始到3之前的部分(包头不包尾)。此时在内存中会开辟另一块空间,切片的本质是一个结构体,一共三部分:切片中首元素在数组中得地址、长度、容量
- 改变切片中得数据会改变数组的值
切片数据结构本质是一个结构体
type slice struct{
ptr unsafe.Pointer
len int
cap int
}
4、值传递和引用传递
在了解切片的传递模式之前先了解下什么是值传递和引用传递
值传递:方法调用时,实际参数把它的值传递给对应的形式参数,方法执行中形式参数值的改变不影响实际参数的值。
引用传递:也称为传地址。方法调用时,实际参数的引用(地址,而不是参数的值)被传递给方法中相对应的形式参数,在方法执行中,对形式参数的操作实际上就是对实际参数的操作,方法执行中形式参数值的改变将会影响实际参数的值。
5、切片是值传递还是引用传递
首先看这段代码的运行结果
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。原切片指向了新的数组
6、切片 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了
}
7、切片的复制
切片复制可以用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过的两个切片的地址是完全不同的
但是等于后的切片,指向的数组还是同一个
8、切片的扩容原理
切片底层的数据结构是一个结构体,里面保存了对底层数组的引用,长度 len,和 容量 cap
不指定 cap的情况下,len 和 cap 是相等的。
后期发生了append 操作的话,当cap 不足时,会进行扩容。cap和len是可能不相等的
8.1 例子
package main
import (
"fmt"
)
func main() {
a := []int{1, 2, 3, 4}
fmt.Println(cap(a))
for i := 0; i < 20; i++ {
a = append(a, a...) // 进行双倍扩容
if len(a) > 1024 { // 当长度大于 1024 的时候我们就退出循环
break
}
fmt.Print(cap(a), " ") // 最后一次扩容
}
fmt.Println(cap(a), len(a))
}
先看结果
- 初始 切片的容量是 4
- 在长度 1024 范围内时,扩容时 均是 进行 二倍扩容
- 在长度超出 1024 范围时,会 进行 25% 扩容,知道 满足容量要求
8.2 源码分析
// src/runtime/slice.go
// go version 1.13
func growslice(et *_type, old slice, cap int) slice {
// ...省略部分
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
//原切片长度低于1024时直接翻倍
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
//原切片长度大于等于1024时,每次只增加25%,直到满足需要的容量
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
// ...省略部分
}
- 可以看到 1024 之前 ,扩容都是
newcap = doublecap
- 大于 1024 时 扩容就变成了循环扩容
for 0 < newcap && newcap < cap { newcap += newcap / 4 }