特性

长度可变、 内容可变、 引用类型、 底层基于数组

定义

var s1 []int               // 长度、容量为0的切片,零值
var s2 = []int{}           // 长度、容量为0的切片,字面量定义
var s3 = []int{1, 3, 5}    // 字面量定义,长度、容量都是3
var s4 = make([]int, 0)    // 长度、容量为0的切片,make([]T, length)
var s5 = make([]int, 3, 5) // 长度为3,容量为5,底层数组为长度为5,元素长度为3

与数组不同,切片的cap和长度len不再相同。

内存模型

 

 

切片本质是对底层数组一个连续片段的引用。此片段可以是整个底层数组,也可以是由起始和终止索引 标识的一些项的子集。

在go的源码(https://github.com/golang/go/blob/master/src/runtime/slice.go)中可以看到切片是有结构体的,切片结构体的标头值有三个:

 

s := []int{1, 3, 5, 7}
fmt.Printf("%v, %p, %p", s, &s, &s[0])
// &a是切片结构体的地址,&a[0]是底层数组的地址。

 

 

 扩容

 append:在切片的尾部追加元素,长度加1。 增加元素后,有可能超过当前容量,导致切片扩容。

 上面示例中,append后导致扩容,返回一个新的切片,但本质上来说返回的是新的Header

 如果增加元素时,当前长度 + 新增个数 <= cap则不扩容, 原切片使用原来的底层数组,返回的新切片也使用这个底层数组,返回的新切片有新的长度,原切片长度不变;

如果增加元素时,当前长度 + 新增个数 > cap则需要扩容, 生成新的底层数组,新生成的切片使用该新数组,将旧元素复制到新数组,其后追加新元素,原切片底层数组、长度、容量不变。

扩容策略

根据原码https://go.dev/src/runtime/slice.go,可以看到,当容量小于256的时候,每次扩容都是翻倍,当容量大于256时,newcap += (newcap + 3*threshold) / 4 计算后就是 newcap = newcap + newcap/4 + 192,即1.25倍后再加192。

PS:(老版本)1.18之前,当扩容后的cap<1024时,扩容翻倍;当cap>=1024时,变成 之前的1.25倍。

 扩容是创建新的底层数组,把原内存数据拷贝到新内存空间,然后在新内存空间上执行元素追加操作。 切片频繁扩容成本非常高,所以尽量早估算出使用的大小,一次性给够,常用 make([]int, 0, 100) 。 

 引用类型

下面是一示例:

 这里在对切片重新定义标识符时,还是值拷贝,不过拷贝的是切片的标头值(Header)。标头值内指针也被复制,刚复制完大家指向同一个底层数组罢了。但是仅仅知道这些不够,因为一旦操作切片时扩容了,或另一个切片增加元 素,那么就不能简单归结为“切片是引用类型,拷贝了地址”这样简单的话来解释了。要具体问题,具体 分析。

Go语言中全都是值传递,整型、数组这样的类型的值是完全复制,slice、map、channel、interface、 function这样的引用类型也是值拷贝,不过复制的是标头值。

 子切片

 

总结

使用slice[start:end]表示切片,切片长度为end-start,前包后不包

start缺省,表示从索引0开始

end缺省,表示取到末尾,包含最后一个元素,start和end都缺省,表示从头到尾

start和end同时给出,要求end >= start

切片刚产生时,和原序列(数组、切片)开始共用同一个底层数组,但是每一个切片都自己独立保 存着指针、cap和len

一旦一个切片扩容,就和原来共用一个底层数组的序列分道扬镳,从此陌路

 

 

posted on 2023-06-05 14:33  自然洒脱  阅读(17)  评论(0编辑  收藏  举报