slice是Golang的动态数组,是一个使用频率极高结构,中文叫切片。其底层记录了数组的指针,当前slice的长度及容量。追加、扩容操作是slice的核心。
slice的结构就不逼逼了,我们直接从追加开始学习。
追加(append)
Golang的append关键字会在中间代码生成阶段的cmd/compile/internal/gc.ssa.go(func (s *state) append(n *Node, inplace bool) *ssa.Value )实现。
使用 append 可以向 slice 追加元素,实际上是往底层数组添加元素。当追加元素致使其长度达到了容量时会触发扩容操作,其判断方法如下。
1 | ptr, len, cap := s |
扩容(growslice)
growslice()在runtime/slice.go中,是slice最重要的方法之一。
growslice在追加(append)期间处理slice的增长。入参为slice元素类型、旧的slice和所需的新最小容量,然后返回至少具有该容量的新slice,并将旧数据复制到其中。
新的slice长度设置为就得slice长度,而不是请求新的容量。
上代码
1 | func growslice(et *_type, old slice, cap int) slice { |
- 先对入参进行一系列的判断。
- 有一个简短、清晰的逻辑计算扩容后容量大小。
1、如果期望容量大于当前容量的两倍就会使用期望容量。
2、如果当前切片容量小于 1024 就会将容量翻倍。
3、如果当前切片容量大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量。这里是个for循环处理。 - 接下来是非常容易被忽略的内容,对类型大小的不同进行特殊处理。
这里实际上就是内存对齐的过程。均需要根据slice的类型size,算出新的容量所需的内存情况capmem,然后再进行capmem向上取整,得到新的所需内存,
除上类型size,得到真正的最终容量,作为新的slice的容量。 - 判断是否会溢出。
- 申请新的空间,并将老数据拷贝到新的空间中。