Go数据结构

本文翻译自《Go Data Structures》。

2009/11/24

在向新程序员解释Go程序时,我发现解释Go值在内存中的样子通常有助于建立正确的直觉,了解哪些操作是昂贵的,哪些不昂贵。这篇文章是关于Go的基本类型、结构体、数组和切片的。

基本类型

让我们从一些简单的例子开始:

变量i的类型为int,在内存中表示为一个32位的字(word)。(所有这些图片都显示了32位内存布局;在当前的实现中,只有指针在64位机器上变长了——int仍然是32位——尽管可以选择使用64位的int64类型。)

由于显式转换,变量j的类型为int32。即使ij有相同的内存布局,它们也有不同的类型:赋值i=j会引起一个类型错误,必须使用显式转换:i=int(j)

变量f的类型为float,当前实现将其表示为32位浮点值。它具有与int32相同的内存占用空间,但内部布局不同。

结构体及其指针

现在情况开始变得有趣。变量bytes的类型为[5]byte,是一个由5个字节组成的数组。它的内存表示就是这5个字节,一个接一个,就像一个C数组。类似地,primes是一个由4个int组成的数组。

Go与C一样,但与Java不一样,它可以让程序员控制什么是指针,什么不是指针。例如,此类型定义:

type Point struct { X, Y int }

定义了一个名为Point的简单结构体类型,表现在内存中就是两个相邻的int字段。

复合字面量语法Point{10, 20}表示已初始化的一个Point实例。获取Point{10, 20}的地址&Point{10, 20}表示指向Point{10, 20}的指针。前者是内存中的两个字(word);后者是指向内存中这两个字的指针。

结构体中的字段在内存中并排排列。

type Rect1 struct { Min, Max Point }
type Rect2 struct { Min, Max *Point }

Rect1是一个具有两个Point字段的结构体,在内存中由一行中的两个Point字段(四个int)表示。Rect2是一个具有两个*Point字段的结构体。

使用过C语言的程序员可能不会对Point字段和*Point字段之间的区别感到惊讶,而只使用过Java或Python(或…)的程序员可能会感到惊讶。通过让程序员控制基本的内存布局,Go提供了控制给定数据结构集合的总大小、分配的元素的数量和内存访问模式的能力,所有这些对于构建性能良好的系统都很重要。

字符串

有了这些预备知识,我们可以继续研究更有趣的数据类型。

(灰色箭头表示字符串实现中存在但在程序中不直接可见的指针。)

一个字符串string在内存中表示为一个2字结构体,里面包含一个指向字符串数据(是一个字节数组)的指针和一个长度字段。由于字符串是不可变类型,因此多个字符串共享同一底层存储是安全的,因此对s进行切片会产生一个新的2字结构体,该结构体具有不同的指针和长度字段,但仍然引用相同的底层字节序列。这意味着可以在不重新分配或复制的情况下进行切片,从而使字符串切片与显式地使用下标索引一样高效。

(顺便说一句,Java和其他语言中有一个众所周知的难题,当你对一个字符串进行切片以保存一小段时,对原始字符串的引用会将整个原始字符串保留在内存中,即使只需要少量的字符串。Go也有这个难题。我们尝试过但拒绝了另一种选择,那就是让字符串切片变得如此昂贵——一次再分配和一个新副本——大多数程序都应该避开它。)

切片(slice)

切片是对某个数组的部分引用。在内存中,它是一个3字结构体,包含指向第一个数组元素的指针、切片的长度和容量。长度是x[i]等索引操作的上限,而容量是x[i:j]等切片操作的上限。

与对字符串进行切片一样,对数组进行切片不会产生新副本:它只会创建一个包含不同指针、长度和容量的新结构体。在本例中,一开始创建切片[]int{2,3,5,7,11}在底层会创建一个包含五个值的新数组,然后设置切片x的字段来描述该数组。但切片表达式x[1:3]没有分配更多的数据:它只是创建一个新的切片头结构体,以引用相同的底层数组。在本例中,它的长度为2,即y[0]y[1]是唯一有效的索引,但容量为4,即y[0:4]是有效的切片表达式。(有关长度和容量以及切片使用方式的详细信息,请参阅Effective Go。)

因为切片是多字结构体,而不是指针,所以切片操作不需要分配内存,甚至不需要为切片头分配内存,因为切片头通常可以保存在栈上。这种切片的使用成本与在C语言中显式传递指针和长度对一样低。Go最初将切片表示为指向上述结构体的指针,但这样做意味着每个切片操作都会分配一个新的内存对象。即使使用快速的内存分配器,也会给垃圾收集器带来很多不必要的工作。不使用指针和分配内存使得切片足够便宜。

new和make

Go有两个数据结构创建函数:newmake。它们的区别在早期是一个常见的混淆点,但似乎很快就变得很自然了。基本区别是new(T)返回一个*T,Go程序可以隐式地解引用该指针(下图中的黑色指针),而make(T,args)返回普通的T,而不是指针。通常,T内部有一些隐式指针(下图中的灰色指针)。new返回一个指向值全是0的一块内存区域的指针(如下图所示),而make返回一个复杂的结构体。

有一种方法可以将这两者统一起来,但这将是对C和C++传统的重大突破:定义make(*T)返回一个指向新分配的T的指针,这样当前的new(Point)就可以被改写为make(*Point)。我们尝试了几天,但认为这与人们对分配函数的期望太不一样了。

更多……

这已经有点长了。接口(interface)、映射(map)和通道(channel)将不得不等待将来的发布。

你应该写坚持写博客,即使没有一个读者

本文翻译自《You should blog even if you have no readers》,作者是开源的分布式实时大数据处理框架Apache Storm的作者。

Spencer Fry的《为什么企业家应该写作》是一篇很棒的文章。我想进一步补充一点,写作的好处是如此非凡,即使你没有读者(无论你是否是企业家),你也应该写博客。

我有50多份未完成的草稿。其中一些只是我和自己争论时写下的一些想法。它们中的大多数永远不会出版,但我从所有这些写作中获得了价值。

写作使你成为更好的读者

博客改变了我阅读别人文章的方式。

在努力寻找正确的方式来构建和展示我的帖子的过程中,我更加适应什么是好的论点,什么是坏的论点。我也变得更善于发现别人推理中的漏洞。

同时,在阅读时,我不太可能陷入以微弱的反驳证据而诋毁帖子的陷阱。在大多数帖子中,都可能有基于特殊案例的反驳。网络评论者喜欢指出这些。然而,这些特殊的案例错过了帖子的主旨。通过理解帖子论点背后的隐含背景,我从阅读中获得了更多的价值。

我也更了解优秀作家的风格。我在脑海中注意优秀作家表达自己想法的方式。我一直很喜欢Paul Graham的写作,但现在我真的很欣赏他组织自己的帖子的方式。他有一种很棒的能力,可以把你吸引到他的世界里,并向你展示他眼中的世界。通过阅读Bradford Cross的博客,我学到了很多关于优秀写作的知识;他的帖子有一个清晰的弧线,很好地利用简短的段落来保持帖子的流畅性。

写作使你更加聪明

写作可以揭示你思维中的漏洞。当你的想法被写下来并回头看时,它们的说服力要比它们在你脑海中时低得多。写作迫使你通过思考和反驳来使你的想法更成熟。

写作可以帮助你以连贯的方式组织你的思想。当这些话题出现时,你就能侃侃而谈。我记不清有多少次我和其他人进行了更深层次的对话,因为我已经让自己的想法变得成熟。

把其他任何东西都视为附带收益

写作给你的其他一切东西——个人品牌、人际网络、工作机会——都只是附带的好处。这些好处有时候非常大,但它们不是你应该写作的主要原因。

你应该写作,因为写作会让你成为一个更好的人。