分布式存储系统:核心技术、系统实现与Go项目实战
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.2 数组和切片

在前文中,我们提到的字节序列实际上是一种一维数组。数组是编程语言中的一个基础结构,是一种顺序集合的表现方式。一个数组主要由两个要素构成:长度和元素类型。在一个数组中,所有的元素类型通常是一致的,并且每个元素占据的空间大小是固定的。这些限制使我们能够通过数组下标直接访问其元素,只需指定下标,编译器就能计算出该元素在内存中的位置。公式如下:

元素的内存位置=数组的起始位置+下标×元素的大小

数组根据长度是否可变分为两种类型:定长数组和变长数组。在Go语言中,这两类数组都有所体现:一类是常规的定长数组,简称为数组;另一类是变长数组,也称为切片(Slice)。这两种类型的数组在初始化和使用方面有着明显的区别,接下来将对这两种数组类型进行更深入的探讨。

提示

在后续Go语言相关的讨论中,如不特殊指定,数组就代表定长数组类型,切片代表变长数组。

2.2.1 数组

在Go语言中,数组的大小在编译时就已确定。Go语言中的数组在结构上和C语言中的数组有些类似,本质上是连续的内存块。一个数组所占的内存大小等于每个元素大小乘以元素的数量。

例如,我们可以定义一个包含8个字节的一维数组,如代码清单2-8所示。

代码清单2-8 一维数组的定义

根据维度不同,数组可以分为一维数组和多维数组。Go语言支持多维数组的定义,例如,二维数组可以被看作行和列的结构,而三维数组则可以被视为具有“长度、宽度、高度”的空间属性。代码清单2-9展示了如何定义一个4×8的二维数组。

代码清单2-9 二维数组的定义

这是一个具有4行8列的二维数组。如果我们想要访问第一行第二列的数据,那么只需要使用arr[0][1]进行访问。值得注意的是,无论数组有多少维度,在内存分配上,其底层实现都是连续的字节序列,维度的概念只是编译器基于实际内存布局所做的逻辑封装。

我们通过一个实例来理解编译后的情况。先定义一个4行8列的二维数组,并进行赋值,如代码清单2-10所示。

代码清单2-10 二维数组的定义和赋值

使用dlv调试工具查看内存信息,使用“x”命令可以查看连续内存块的内容。以下是使用dlv命令打印数组twoDimensional的内容的例子:

从打印出的内存信息可以清楚地看出,0x01、0x02、0x03被赋值到了前3个字节,而0x04、0x05、0x06则被赋值到了第9个字节开始的3个字节位置。这个二维数组连续分配了32字节的内存空间,然后编译器在此基础上实现了多维数组的逻辑抽象。

2.2.2 切片

在Go语言中,切片是一种表达可变长度数组的核心数据结构。切片由两部分组成:一个是24字节的元数据(即切片的Header),另一个是可变长度的底层数组。切片的元数据定义如代码清单2-11所示。

代码清单2-11 切片的元数据定义

在这里,array字段用于存储被管理元素的起始地址,len字段代表切片中实际的元素个数,而cap字段则指示分配的总元素空间。这两个字段的值可以分别通过Go的内置函数len()和cap()来获取。一般而言,cap的值会大于len,这表示切片有额外的预分配空间。值得注意的是,切片元数据和底层数组通常位于内存的不同区域,它们一般是不连续的。图2-4直观地展示了切片的结构。

图2-4 切片结构示意

接下来,我们来了解一下切片的初始化过程。

(1)使用var关键字创建切片

在Go语言中,结构体的内存分配保证了初始状态下所有字段均为零值。当我们通过var声明一个切片变量时,只分配了切片元数据的内存,底层数组的内存尚未分配。因此,在这种情况下使用切片时需要格外小心。

例如,下面的代码展示了使用var直接创建一个切片变量,该方式只分配了一个24字节的切片元数据,并且切片元数据的各字段都为零值。

(2)使用make创建长度为0的切片

还有一种方法是通过make来创建一个长度为0的切片,这种方式也只分配了一个24字节的管理元数据内存。如下所示:

采用这种方式创建的切片,切片元数据的array字段会指向一个特殊的变量地址(runtime.zerobase)。zerobase变量也被空结构体使用,主要应用于需要引用地址而不实际分配内存的场景。与使用var直接定义相比,这种方式加入了初始化的动作,编译器会在编译时将make转换为对makeslice函数的调用。

从内存分配的角度来看,以上两种方式创建的切片都只包含切片元数据。此时的切片不能直接通过索引的方式存取元素。但是,当调用append函数添加元素时可以直接使用它们,因为编译器会将对append函数的调用转换成对growslice函数的调用。在growslice函数中,会自动处理内存分配问题。如果切片没有分配底层数组的空间则会进行分配,如果容量不足则会进行扩容。

(3)使用make创建指定大小的切片

在常见的用法中,可以在创建切片时指定切片长度,示例如下:

通过这种方式创建的切片包含两个部分:一部分是24字节的切片元数据,另一部分是包含2字节元素的底层数组。由于元素类型是字节,因此底层数组总共占用2字节。

(4)使用make创建指定大小的切片并预分配空间

另一种创建切片的方法是预分配超出初始长度的内存空间。如果已知切片将来会增长到某个特定大小,就可以预先分配足够的内存空间,这样在切片扩容时,可以避免重新分配内存,直接使用预分配的空间。在性能优化的场景中,这种方式非常有效,因为它省去了临时分配内存的时间开销。示例代码如下:

总的来说,上面提到的四种初始化切片的方式对应的切片结构如图2-5所示。

图2-5 四种初始化切片的方式

切片的元素类型可以是任意的,其中字节类型的切片是最常见且核心的结构,它也是I/O操作中的核心参数之一。

提示

除非特别指明,本书提到的“字节序列”默认是指字节类型的切片。