go从零开始2 [slice]
继上一篇map之后,我们继续对slice类型的详细梳理。
正式开始之前,先抛几个问题,
func main() {
question()
question1()
question2()
}
func question() {
// 问题1
slice := []int{1, 2, 3, 4}
mem := slice[1:1]
fmt.Println(mem) // 输出什么 结果:[]
fmt.Printf("%T", mem) // 输出类型是什么 结果:[]int
}
func question1() {
// 问题2
slice := []int{1, 2, 3, 4, 5}
slice2 := append(slice[:3], 6, 7)
fmt.Println(slice2) // 输出什么 结果:[1 2 3 6 7]
fmt.Println(slice) // 输出什么 结果:[1 2 3 6 7]
newSlice := []int{1, 2, 3, 4, 5}
newSlice2 := append(slice[:3:3], 6, 7)
fmt.Println(newSlice2) // 输出什么 结果:[1 2 3 6 7]
fmt.Println(newSlice) // 输出什么 结果:[1 2 3 4 5]
}
func question2() {
fu0 := func(array [5]int) {
array[0] = 9
fmt.Printf("fu1 数组值: %v, fu1 数组内存地址:%p \n", array, &array)
}
fu1 := func(l []int) {
l[0] = 9
fmt.Printf("fu1 slice值: %v, fu1内 slice 内存地址:%p \n", l, l)
}
// 问题 3: 切片传值问题
array := [5]int{1, 2, 3, 4, 5}
fmt.Printf("数组:%v , 数组内存地址:%p \n", array, &array)
fu0(array)
fmt.Printf("数组:%v , 数组内存地址:%p \n\n", array, &array) //思考为啥不变?
slice := array[0:]
fmt.Printf("slice值:%v , slice 内存地址:%p \n", slice, slice)
fu1(slice)
fmt.Printf("slice值:%v , slice 内存地址:%p \n", slice, slice) //思考为啥变了?
fmt.Printf("数组:%v , 数组内存地址:%p \n\n", array, &array) //what 为啥又变了。。
/* 打印结果
数组:[1 2 3 4 5] , 数组内存地址:0xc00012c030
fu1 数组值: [9 2 3 4 5], fu1 数组内存地址:0xc00012c090
数组:[1 2 3 4 5] , 数组内存地址:0xc00012c030
slice值:[1 2 3 4 5] , slice 内存地址:0xc00012c030
fu1 slice值: [9 2 3 4 5], fu1内 slice 内存地址:0xc00012c030
slice值:[9 2 3 4 5] , slice 内存地址:0xc00012c030
数组:[9 2 3 4 5] , 数组内存地址:0xc00012c030
*/
}
上面的问题如果你们都懂了,恭喜你完全理解了slice~
不懂的盆友,可以继续往下看
要理解的slice,首先要先理解array。那么在go中array是一个什么样的存在呢? 其实array就是go在底层中开辟的一个连续的内存空间,那么切片(slice)就是对这串连续空间的引用。有的兄弟会说,哎呀老哥,你这说的有点抽象呀,我还是不太理解,咋滴搞嘛。。 嗯。。那我们就看看slice的底层结构,再来理解一下。
// slice的底层结构
type slice struct {
array unsafe.Pointer //嗨,胸弟,这就是~ 这是个指针,指向了底层的数组
len int // 切片的长度
cap int // 切片的容量
}
现在知道切片到底是个什么东西了。我们来看看切片是如何创建
要么从数组或者切片去切分或者自己声明去创建。
//声明创建slice
var slice []int
var slice = []int{}
var slice []int = nil
slice := make([]int, len, cap)
//通过截取的方式
array := [...]int{1,2,3,4,5}
//start为起始下标 len位置为空,则自动使用截取切片或数组的长度。cap位置为空,则自动使用截取切片或数组的容量。
//slice 的 len(slice) = len-start
//slice 的 cap(slice) = cap-start
slice := array[start : len : cap]
说了这么多。。所以问题一的总结来了。
- 获取切片元素时,下标最大值为len(array)-1。
- 截取切片时,起始下标可为len(array),len最大为array的长度,cap最大为array的容量。起始下标为len(array) 返回array类型的空值。其他情况系统报错。
都到这里有没有理解切片引用底层数组的逻辑呢。。。 (正在看书的小涛说:没有!!!)我们继续~
那我们解析一下上述代码。
初始化一个切片
slice := []int{1, 2, 3, 4, 5}
slice2 := append(slice[:3], 6, 7) 这一步是对切分出来的切片进行元素的添加,拆分下
tmp := slice[:3] 这一步实际的操作是 slice[0:3:5] 那得到的是什么结果呢?
值的结果为:[1,2,3] 但是因为容量为5,模拟下值:[1,2,3, $1, $2]
$i相当于是slice[3]的内存地址,$2相当于slice[4]的内存地址。
所以 append添加6,7后。导致slice的值发生了变化。
文字描述都是浮云,我们上图为证
图中所示 len(months) = 13 cap(months) = 13。Q2 = months[4:7] 因为截取时,cap为空,根据上述的规则 cap(Q2) = cap(months)-start 即 cap(Q2) = 13 - 4 = 9 。
同理 summer[6:9] , cap(summer) = cap(months)-start 即 cap(summer) = 13 - 6 = 7
继续探讨我们的问题2
slice := []int{1, 2, 3, 4, 5}
tmp := slice[:3:4]
tmp = append(tmp, 6)
tmp = append(tmp, 7)
这个时候tmp和slice分别又是什么呢?为什么?
我们先思考两分钟,然后我来公布答案。。
叮叮叮~ 两分钟到啦~
slice = [1,2,3,6,5]
tmp = [1,2,3,6,7]
为什么会这样呢?
原因是tmp的容量为4,在添加6的时候,tmp最后的位置和slice是同一块地址,所以同时更新为6,tmp继续添加7的时候,因为tmp容量不够了,需要进行扩容。扩容操作会导致底层数组的变更,所以slice中的5 不会改变。切片的扩容符合一下规则:
- 切片容量小于1024时,倍增扩容
- 切片容量不小于1024时,按1.25倍扩容。
来,让我们检验一下。上~ 代码~
func testLenCap() {
var sliceTest []int
fmt.Printf("init: len=%d cap=%d \n", len(sliceTest), cap(sliceTest))
for i := 1; i < 10000; i++ {
innerCap := cap(sliceTest)
innerPro := float32(0)
sliceTest = append(sliceTest, i)
if innerCap != cap(sliceTest) {
if innerCap != 0 {
innerPro = float32(cap(sliceTest)) / float32(innerCap)
}
fmt.Printf("init: len=%d cap=%d rate=%f \n", len(sliceTest), cap(sliceTest), innerPro)
}
}
}
为了便于观察,我们只在容量变化时,进行打印,打印结果如下:
init: len=0 cap=0
change: len=1 cap=1 rate=0.000000
change: len=2 cap=2 rate=2.000000
change: len=3 cap=4 rate=2.000000
change: len=5 cap=8 rate=2.000000
change: len=9 cap=16 rate=2.000000
change: len=17 cap=32 rate=2.000000
change: len=33 cap=64 rate=2.000000
change: len=65 cap=128 rate=2.000000
change: len=129 cap=256 rate=2.000000
change: len=257 cap=512 rate=2.000000
change: len=513 cap=1024 rate=2.000000
change: len=1025 cap=1280 rate=1.250000
change: len=1281 cap=1696 rate=1.325000
change: len=1697 cap=2304 rate=1.358491
change: len=2305 cap=3072 rate=1.333333
change: len=3073 cap=4096 rate=1.333333
change: len=4097 cap=5120 rate=1.250000
change: len=5121 cap=7168 rate=1.400000
change: len=7169 cap=9216 rate=1.285714
change: len=9217 cap=12288 rate=1.333333
相信看到这里你已经了解了切片和数组之前的爱恨情仇。那我们继续补充一点,当数组和切片作为参数时的场景,也就是问题3
首先明确一点,那就是函数的参数是以值拷贝的形式传递的。
首先传递的数组,在函数体内修改数组,发现数组值没有发生变化,且根据打印的地址,确实不一致。
第二次调用 传切片,可以发现外面的切片的值发生了变化,且根据打印的内存地址发现,函数内和函数外是相同的内存地址,所以对待切片要深刻理解 :切片是底层数组引用。还有就是在向函数传值时传递的是引用,函数仍然也是值拷贝,只不过是对引用的拷贝罢了,两个值同时指向一个地址。
好了~~ 到这里基本把切片剥好了~ 就等大家使用啦
文章如有错误,请各位大佬不吝指教~