声明
var identifier []type
var slice1 []type = make([]type, len)
var slice1 []type = make([]type, len, cap)
s :=[]int{0,1}//s := [len]int{} 和数组有二义性不允许
s := make([]int, len) //len可以是变量
var numbers []int //这样的 slice == nil,这也是slice唯一合法的比较操作
底层实现!!!
切片,一开始看感觉和c++的vector很像,但实际上还是有区别的,slice的底层比vector简单得多,slice更像是对数组的一层封装而vector是对线性内存空间进行操作
切片内部结构:
struct Slice
{
byte* array; // actual data
uintgo len; // number of elements
uintgo cap; // allocated number of elements
};
//所以unsafe.Sizeof(切片)是 24
当把 slice 作为参数,本身传递的是值,但因为 byte* array指针指向的还是原底层数组,所以对参数的修改也会影响到外部。
但如果对 slice 本身做 append,而且导致 slice 进行了扩容,实际扩容的是函数内复制的一份切片,对于函数外面的切片没有变化。
细品上面这句话,如需要扩容则会开辟新的内存并且copy原来的arr,这时append返回的slice的底层数组和原来的不是同一个
如果不需要扩容那么还是在原有的底层数组进行操作
len和cap函数
和vector的size和capacity类似,len切片<=cap切片<=len底层数组
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) //打印一个空切片 len=0 cap=0 slice=[]
切片操作
可以通过切片操作来引用数组/切片
s := arr[s:l] //将 arr 中[s,l)的元素创建为一个新的切片
s := arr[:l] //[0,l)
s := arr[s:]//[s, end)
s := arr[:] //相当于s引用arr
注意,对于slice,切片操作返回一个原始字节系列的子序列,底层都是共享之前的底层数组,因此切片操作对应常量时间复杂度
所以上图中一旦修改了Q2或者summer,mouths也会被改变
append和copy
func main() {
var numbers []int
printSlice(numbers) //len=0 cap=0 slice=[]
/* 允许追加空切片 */
numbers = append(numbers, 0)
printSlice(numbers) //len=1 cap=1 slice=[0]
/* 向切片添加一个元素 */
numbers = append(numbers, 1)
printSlice(numbers) //len=2 cap=2 slice=[0 1]
/* 同时添加多个元素 */
numbers = append(numbers, 2,3,4)
printSlice(numbers) // len=5 cap=6 slice=[0 1 2 3 4]
/* 创建切片 numbers1 是之前切片的两倍容量*/
numbers1 := make([]int, len(numbers), (cap(numbers))*2)
/* 拷贝 numbers 的内容到 numbers1 */
copy(numbers1,numbers)
printSlice(numbers1) //len=5 cap=12 slice=[0 1 2 3 4]
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
append切片
s = append(s, t...) //t should be slice
比较
slice唯一合法的比较操作是和nil比较
如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断,因为一个零值的slice等于nil。
扩容
可以通过查看$GOROOT/src/runtime/slice.go源码,其中扩容相关代码如下:
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
从上面的代码可以看出以下内容:
- 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
- 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),
- 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
- 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。
需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如int
和string
类型的处理方式就不一样。