Go Data
- 原文地址:Go Data Structures
- 原作者:Russ Cox
- 原文发表于:2009-11-24
- 所有图片均来源于原文
- 译者注:由于本文年代久远,部分内容可能已经过时,请读者自行甄别。译者在能力范围之内,会在译文中进行标注。
在向新程序员解释 Go 语言时,我发现解释 Go 的值在内存中的样子非常有用,这有助于他们建立正确的直觉来判断哪些操作开销大,哪些操作开销小。本文将说明基础类型、结构体、数组和切片。
基础类型
先看一些简单的例子:

变量 i 的数据类型为 int,在内存中表示为一个 32 位的字。(所有这些图片显示的都是 32 位的内存布局;在当前的实现中,只有指针在 64 位机器上会大一些。int 依然是 32 位,虽然它的实现可以选择使用 64 位来代替。)
由于显式的转换,变量 j 的数据类型为 int32。尽管 i 和 j 有着相同的内存布局,但它们的数据类型却不同。赋值操作 i = j 将会导致类型错误,必须显式地进行类型转换:i = int(j)。
变量 f 的数据类型为 float,当前实现将其表示为 32 位浮点数。它与 int32 一样占用 4 个字节,但其内部布局则完全不同。
结构体和指针
接下来也是同样的,变量 bytes 的数据类型是 [5]byte,包含 5 个字节的数组。它在内存中表现为简单的 5 个字节,一个紧挨着另一个,和 C 语言中的数组一样。同样的,primes 是一个包含 4 个 int 的数组。
Go 语言,像 C 语言而不是 Java,给予程序员完整的控制而且不是通过指针。例如有如下类型定义:
|
这里定义了一个简单的结构体名为:Point,在内存中表现为两个相邻的整数:

Point{10, 20} 这样一个复合字面量语法表示一个正确初始化的 Point。对这样的复合字面量取地址,表示为一个指向初始化后 Point 的指针。前一种定义方式表示内存中的两个 字,而后一种定义方式表示一个指向内存中两个字的指针。
结构体中的字段在内存中被一个紧挨另一个地平铺。
|

Rect1 包含两个 Point 字段,其在内存中的表现形式为 2 个 Point 即 4 个 ints 在同一行中。Rect2 包含两个 *Point 字段,其表现形式为两个 Point 指针。
C 程序员应该不会对 Point 和 *Point 之间的区别感到惊讶,而使用 Java 语言、Python 语言或者其他别的语言的程序员或许会。通过给予程序员对基本内存布局完整的控制,Go 语言提供了能力去控制给定数据结构集合总的大小,以及内存访问模式,所有这些对于构建性能良好的系统都很重要。
字符串
有了前边的这些铺垫,我们将继续研究更多有意思的数据类型。

(灰色箭头表示在实现中存在但在程序中不直接可见的指针。)
字符串在内存中表示为 2 个字 的结构,其中包含指向字符串数据的指针和字符串的长度。 由于字符串是不可变的,所以多个字符串共享相同的底层字节序列是安全的,因此分割 s 会导致新的一个 2 个字 的结构 t,其指针和长度与 s 不同,但仍指向相同的底层字节序列。 这意味着分割切片无需分配或复制即可完成,从而使字符串切片的效率与使用索引一样高效。
(顺便说一句,在 Java 和其他语言中有一个众所周知的陷阱,当对字符串进行切片以保存其中一小段时,对原始字符串的引用会导致整个原始字符串始终保留在内存中,即使我们只需要其中的一小部分。 Go 语言也有这个陷阱。 我们尝试过并最终放弃的另一种选择是重新分配内存并复制所需的部分,但这使字符串分割切片操作开销很大,以至于大多数程序都避免使用它。)
切片

切片是对一个数组其中一段的引用。在内存中表现为 3 个字 大小的结构体,包含指向第一个元素的指针,切片的长度以及容量。其中长度是切片索引操作的上限,如 x[i],容量则是分割操作的上限,如 x[i:j]。
如同分割字符串那样,分割一个数组也不会导致数据复制,而仅仅是创建了一个新的结构体来保存指向第一个元素的指针、长度及容量。例如上图所示:复合字面量 []int{2, 3, 5, 7, 11} 创建了一个新的包含 5 个元素的数组,设置 x 切片中相应的字段,然后用 x 来描述这个切片。表达式 x[1:3] 并没有占用更多的内存空间,而只是创建了一个新的切片结构体,填充其中的数据,然后指向同样的底层数据序列。切片 y 中有效的索引为 y[0] 和 y[1],而分割操作 y[0:4] 也是合法的表达式操作。
由于切片是多字结构,而不是指针,因此切片操作无需分配内存,甚至不需要为切片标头分配内存,通常可以将其保留在堆栈中。 这种表示形式使切片比在 C 语言中传递显式指针和长度开销要小很多。Go 语言最初将切片表示为指向上述结构的指针,但是这样做意味着每个切片操作都会分配一个新的内存对象。 即使使用快速分配器,也为垃圾收集器制造了许多不必要的工作,并且我们发现,与字符串一样,程序避免切片操作,而是希望传递显式索引。 在大多数情况下,移除间接寻址和分配将使分割切片的开销笑到足以避免传递显式索引。
New 和 Make
Go 语言有两个进行数据结构创建的函数:new 和 make。它们之间的区别在开始可能会造成困扰和疑惑,但理解之后这一切就变得自然了。最基本的一个区别是,new(T) 返回的是 *T,一个可以隐式解引用的指针(如下图中的黑色箭头),而 make(T, args) 返回一个普通的 T,而不是指针。T 中经常包含一些隐式指针(如下图中的灰色箭头)。new 返回一个指向零值的指针,而 make 返回一个复杂的结构。
有一种统一两者的方法,且与 C 语言或者 C++ 语言大相径庭:使用 make(*T) 来返回一个指针指向刚刚创建的 T,这样一来 new(Point) 可以写为 make(*Point)。我们尝试过几天,但认为它与人们对分配功能的期望相差太大。
下一步
写到这里文章似乎已经有点长了。interface、maps 以及 channels 将在后续的文章中阐述。
最后更新: