数组是什么?

go的数组是一种相同类型元素的集合,内存分配为一块连续的内存,数组的组成分为数组类型和数组长度,注意:go的数组长度是数组的一部分,对比数组是否相同,也要比对数组长度。

数组声明的方式?

切片是什么

切片可以理解为动态数组,声明时只需要指定类型即可。

数据结构

  1. type SliceHeader struct {
  2. Data uintptr // Data 是指向数组的指针;
  3. Len int //Len 是当前切片的长度;
  4. Cap int //Cap 是当前切片的容量,即 Data 数组的大小:
  5. }

切片理解成一片连续的内存空间加上长度与容量的标识。
切片在运行时才会确定内容结构,所以操作需要依赖go语言的运行时。

切片初始化

1. 直接声明

var slice []int
创建出来的是 nil slice ,长度和容量都是0.和nil比较结果为true。
容易混淆的是 empty slice,它的长度和容量也都是0,所有的空切片都指向同一个地址,空切片创建方式

  1. slice := make([]int,0//或者
  2. slice := []int{}

内部结构图如下
slice-emptyslice-nilslice.webp

创建方式 nil切片 空切片
方式一 var s1 []int var s2 = []int{}
方式二 var s4 = *new([]int) var s3 = make([]int, 0)
长度 0 0
容量 0 0
和 nil 比较 true false
json序列化 null []

官方标准建议:避免写代码的时候把脑袋搞昏的最好办法是不要创建「 空切片」,统一使用「 nil 切片」,同时要避免将切片和 nil 进行比较来执行某些逻辑。

1. 下标方式

编译器转化成OpSliceMake操作。SliceMake 操作会接受四个参数-参数类型、数组指针地址、切片大小和容量。
注意:下标初始化切片不会拷贝原数组或者原切片中的数据,它只会创建一个指向原数组的切片结构体,所以—修改新切片的数据也会修改原切片。

2. 字面量方式

[]int{1,2,3} 创建新切片, cmd/compile/internal/gc.slicelit 函数操作步骤

3. 关键字-make

调用make关键字,传入切片大小、容量、然后进行类型检查。
类型检查期间进行校验入参:
/src/cmd/compile/internal/gc/typecheck.go#L326

  1. # 1.切片的大小和容量是否足够小
  2. # 2.切片是否发生了逃逸,最终在堆上初始化
  3. func typecheck1(n *Node, top int) (res *Node) {
  4. ...
  5. }

如果切片发生了逃逸,运行时需要runtime.makeslice在堆上初始化切片。如果非常小不逃逸直接下标方式

访问元素

切片的基本操作都是在编译期间完成的。

追加和扩容

追加扩容分为两种逻辑,append返回的新切片需不需要赋值会原有的变量。两种逻辑差不多,差距点在于是否需要赋值回原有变量。

  1. # /src/runtime/slice.go#L125
  2. # 以下部分代码仅确定大致容量
  3. func growslice(et *_type, old slice, cap int) slice {
  4. newcap := old.cap
  5. doublecap := newcap + newcap
  6. if cap > doublecap {
  7. newcap = cap
  8. } else {
  9. if old.len < 1024 {
  10. newcap = doublecap
  11. } else {
  12. for 0 < newcap && newcap < cap {
  13. newcap += newcap / 4
  14. }
  15. if newcap <= 0 {
  16. newcap = cap
  17. }
  18. }
  19. }

扩容策略:

  1. 如果期望容量大于当前容量的两倍就会使用期望容量;
  2. 如果当前切片的长度小于 1024 就会将容量翻倍;
  3. 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;

如果切片中元素所占字节为1、8或者2的倍数时。然后再进行内存对齐。对齐一般是内存向上取整。

拷贝切片

copy(a,b) —-> cmd/compile/internal/gc/walk.go-copyany函数分为两种情况

  1. 当前copy不是运行时调用的。-编译期间拷贝
  2. 拷贝是在运行时发生的。

打切片拷贝操作时一定要注意对性能的影响。

小结

切片的很多功能斗士由运行时实现的。 切片初始化、切片追加或扩容都需要运行时操作。 大切片扩容活复制或者大规模内存拷贝是,一定要减少类似操作,避免影响程序的性能。

reference

数组、切片追加机制)