slice 又称动态数组,依托数组实现,但比数组更灵活,在 Go 语言中一般都会使用 slice 而非 array。
特性
声明和初始化 slice 的方式除了从数组中直接切取外,主要还有以下几种:变量声明、字面量声明、使用内置函数 new() 和 make(),一般推荐使用 make() 函数来初始化。
实现原理
数据结构
源码 src/runtime/slice.go:slice 定义了 slice 的数据结构:
type slice struct {
array unsafe.Pointer // 底层数组指针
len int // 长度
cap int // 容量
}
相关操作
创建
当我们使用 make() 创建 slice 时,可以同时指定长度和容量,底层会分配一个数组,数组的长度即容量。
例如,slice := make([]int, 5, 10)
所创建的 slice,结构如下所示:
扩容
向 slice 追加元素时,如果 slice 的容量不足以容纳追加的元素时,将会触发扩容。扩容实际上是重新分配一块更大的内存,将原 slice 拷贝进新 slice,再将数据追加进去。这也是为什么我们经常能见到类似于 s := append(s, 1)
这种写法的原因。
扩容时容量的变化遵循以下基本规则:
- 原 slice 容量小于 1024,则新 slice 的容量扩大为原来的 2 倍;
- 原 slice 容量大于等于 1024,则 新 slice 的容量扩大为原来的 1.25 倍。
不过,实际过程中还会综合考虑元素的类型及其内存分配策略,在该规则的基础上做一些微调(如内存对齐)。
注意:自从 1.18 版本开始,slice 的扩容算法进行了优化,在容量不超过 256 时,还是翻倍扩容,超过 256 时增加的容量则随着 slice 本身容量的增加缓慢收敛于 25%(例如容量为 512 的 slice 将增长 63%,但容量为 4096 的 slice 仅增长 30%)。
下面有一段代码示例,观察函数的输出,结合本部分内容,可以很好地明白扩容的内部机制:
package main
import "fmt"
func SliceRise(s []int) {
s = append(s, 0)
for i := range s {
s[i]++
}
}
func main() {
s1 := []int{1, 2}
s2 := s1
s2 = append(s2, 3)
SliceRise(s1)
SliceRise(s2)
fmt.Println(s1, s2)
}
拷贝
拷贝两个 slice 时,会将源 slice 的元素逐个拷贝到目的 slice 指向的底层数组中,拷贝数量取决于两个 slice 长度的较小值。例如长度为 10 的 slice 拷贝到长度为 5 的 slice 中时,只会拷贝 5 个元素,过程中并不会触发扩容。
一些Tips
我们常说 slice 是所谓的引用类型,主要就是因为它的结构内部含有底层数组的指针,因此在函数内部修改 slice 中的元素的话,外部变量也会受到影响。
我们可以使用 a[low:high]
表达式切取字符串,此时会产生新的字符串,用这个方法可以快速截取字符串。
如果我们是从字符串或数组上切取 slice 的话,表达式 a[low:high]
需满足: 0 <= low <= high <= len(a),如果不满足则会触发 panic。但是如果切取对象是另一个 slice 的话,low 和 high 的最大取值就不是 a 的长度,而是 a 的容量。在实际使用中,我们需要注意这一点。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。