深度剖析Go语言数据结构

jopen 11年前

当向一个新程序员解释Go语言时,我发现如果解释Go的数据是如何在内存中表示的,将有助于建立编写高效程序的良好直觉。


基础类型

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


深度剖析Go语言数据结构


变量i是int类型,在内存中占用一个32位的存储单位。(上图拿32位系统来举例;对以上的例子,只有指针才会在64位的机器上占用更多的空间——int始终是32位——然而我们仍然可以选择64位的系统。)

变量j是int32类型,因为它经过了显式的类型转化。尽管i和j有着同样的内存布局,但它们的类型是不一样的:像这样的赋值i = j会产生类型异常,必须通过显式的类型转换:i = int(j)   。

变量f是个浮点类型,上例中它代表着占用32位的浮点值。它的内存占用跟int32一样,但内部布局不同。

结构与指针

 变量bytes是[5]byte类型,一个具有5个字节的数组。它的内存表示就只有5个紧挨着的字节,就像C里的数组一样。相似地,primes变量是一个拥有4个int数值的数组。

Go就像C而不像Java,它让程序员决定什么是或者不是一个指针。拿这个类型定义来举例:

1 type Point struct { X , Y int }

定义一个叫Point的简单的结构类型,意味着内存里是两个相邻的int。


深度剖析Go语言数据结构


Point{ 10, 20 }这句复合语法表示一个被初始化的Point对象。而&Point{ 10, 20 }这句则表示一个指向被初始化的Point对象的指针。前者在内存中有两个数据块,而后者则存放着一个指向两个数据块的指针。

结构中的字段被依次地排列在内存里面。

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


深度剖析Go语言数据结构


Rect1是一个拥有两个Point类型字段的结构,它的一条记录包含了两条Point记录——共4个int。Rect2是一个拥有两个Point类型指针的结构,在内存里它占两个Point指针的空间。

用过C的程序员也许对Point字段和*Point字段的区别并不陌生,而只用过Java或者Python(或者其他)则可能为需要做出选择而惊讶。通过 为程序员提供控制基础内存布局的可能,Go语言让程序员可以操控所有数据结构总尺寸、所分配变量的总数和内存访问的模式,这些对于建造高性能系统都至关重 要。


字符串

接下来我们继续看一些更有趣的数据类型。


深度剖析Go语言数据结构


(灰色箭头意味着实现上的真实表示方式,但这在编程过程中是不可见的)


一个字符串在内存中的表示被分成两段,一个指向字符串数据的指针和一个长度值。因为字符串是可枚举的,所以多个字符串共享同一段存储空间也是安全的,因此 如果对s字符串进行一个切片选择,将得到一个可能不一样的指针和长度,但它们也指向同一段字节序列。这意味着,切片并不需要分配空间或者是复制数据,创建 切片很容易,只需要传递明确的下标值就行了。

(顺带一句,在Java和某些与严重有一个著名的缺陷,当你对一个字符串进行切片并保存其中的一小部分,引用将在内存中保存原字符串的完整内容,即使只有 很小的一部分是被用到的。Go也有这个缺陷。要不然(我们尝试但最终舍弃了),我们将对切片采取昂贵的做法——分配内存并拷贝数据——大部分语言都避免这 种做法。


切片


深度剖析Go语言数据结构


切片是对数组中一段数据的引用。在内存中它有三段数据组成:一个指向数据头的指针、切片的长度、切片的容量。长度是索引操作的上界,如:x[i] 。容量是切片操作的上界,如:x[i:j]  。

跟对字符串做切片一样,对数组进行切片也不会导致复制:它只创建一个存放指针、长度和容量的结构体。在这个例子中,语句[ ] int { 2, 3, 5, 7, 11 } 创建了一个包含5个值的新数组,并为x切片设置了对应的值来描述那个数组。切片表达式x[1:3]并不为数据分配内存:它只填充切片结构的字段,用以复用 数组的存储空间。在这个例子中,长度是2,y[0]和y[1]是仅有的合法数据;但容量是4,y[0:4]是个合法的切片表达式。(查看高效GO获取更多关于长度、容量,以及如何使用切片的信息。)

由于切片不是指针而是多字段的结构,切片操作并不需要分配内存,即使对于切片头也是这样,它可以常驻在栈中。这种表示法让切片的使用的代价很低,就像C中 传递精确的指针和长度一样。Go原生地在切片中使用了指针,这也意味着每个切片操作都分配一个内存对象。即使有了一个更快的内存分配器,这为垃圾回收带来 了不必要的工作,并且我们发现,就像字符串那个例子一样,给于精确的下标,比进行切片操作好。大多数情况下,避免不必要的间接引用和内存分配可以让切片足 够高效了。


new和make

Go有两种创建数据结构的方法:new和make 。它们的区别是常见的早期困惑,但很快就会变得自然。基础的区别在于,new(T)返回一个*T类型,一个可以被隐性反向引用的指针(如图中的黑色指 针),而make(T,args)返回一个原始的T,它并不是一个指针。T中常有写隐性的指针(如图中的灰色指针)。new返回一个指向初始化为全0值的 指针,而make返回一个复杂的结构。


深度剖析Go语言数据结构


有一种方式让两者统一起来,它对于传统的C和C++是一个重大的改变:定义make(*T)来返回一个指向新分配的T的指针,因此new(Point)和 make(*Point)的效果是一致的。我们用这种方法尝试了一段日子,但最终觉得这对于一些期待一个分配函数的人来说,实在太难以接受了。

中文原文地址:http://www.zingscript.com/post/195

英文原文地址:http://research.swtch.com/godata