slice原理分析
slice也称动态数组
make格式创建
s1 := make([]int,12) // 指定长度
s2 := make([]int,10,100) // 指定长度len和容量cap
字面量形式
s3 := []int{} // 需要注意空切片其值不是nil
s4 := []int{1,2,3} // 长度为3的切片
从数组或者切片中截取切片
arr := [5]int{1, 2, 3, 4, 5}
s5 := arr[0:2] // 左闭右开
s6 := s5[0:1] // 从切片中切取
需要特别注意的是,如果修改切片中的元素,会递归影响从原数组或者切片
再提一点,使用new函数创建切片
s7 := *new([]int)
此时创建的切片的值为nil
关于切片的操作
s := make([]int, 0)
fmt.Println(len(s), cap(s))
fmt.Printf("%p\n", s)
s = append(s, 1)
fmt.Printf("%p\n", s)
s = append(s, 2, 3, 4)
fmt.Printf("%p\n", s)
s = append(s, []int{5, 6}...)
fmt.Printf("%p\n", s)
fmt.Println(s)
0 0
0x118f390
0xc000124020
0xc000126020
0xc000132000
[1 2 3 4 5 6]
可以看到每操作一次append都是新建一个切片,然后把原切片的内容复制过去,然后返回新的切片
直接来看slice的数据结构
type slice struct {
array unsafe.Pointer // 指针指向切片在内存中的起始地址
len int // 长度
cap int // 容量
}
前面也提到使用数组来构建切片,下面可以证明数组和数组的切片共享底层存储空间
array := [10]int{}
slice := array[5:7]
fmt.Println(len(slice), cap(slice))
2 5
关于slice扩容机制,涉及内存分配原则:
- 原slice的容量小于1024,新切片的容量将扩大到原来的2倍
- 原slice的容量大于1024,新切片的容量将扩大到原来的1.25倍
这么做有以下两点好处:
- 当切片容量较小时,采用较大的扩容倍速,可以避免频繁扩容,减少内存分配的次数和数据拷贝的代价,这里会涉及到元素的搬迁,费时费空间。
- 当切片容量较大时,采用较小的扩容倍速,可以有效避免内存空间的浪费。
扩容的步骤:先将slice扩容,得到新的slice,再将新元素追加到新slice中,返回新的slice。
切片表达式
a[low : high]
a[low : high : max] // 这里的max将限定切片的容量为 max-low, low可以省略,而high和max不可以省略
low和high的最大值应为切片的容量
这里有会引伸出一个问题,对于扩展表达式,向限制最大容量的切片中append元素,如果容量不够那么会产生一个新的切片,而不是覆盖原始的数组或切片。