向新程序员讲解 Go 语言的时候,必要的是解释 Go 各种数据值在内存中的组织来给他们建立正确的思想,知道哪些操作是开销昂贵的,哪些是不昂贵的。这篇文章就是关于基本类型,结构,数组和切片的内部实现原理。
基本类型
让我们从一个简单的例子开始
图1
变量 i 的类型是 int, 在内存中用一个32比特位的字来表示。j 的类型是 int32 ,由于显示类型转换。即使 i 和 j 由相同的内存布局,他们本质上还是不同的类型:在 Go 中直接 i = j 赋值将会产生一个错误,我们必须显示的转化 j,i = int(j)。
值 f 的类型是 float,在内存中用 32 比特位的浮点值格式表示,它和 int32 有相同的内存使用量但是内部布局是不同的,感兴趣的可以去了解一个浮点数如何在内存中表示。
结构体和指针
图1中 bytes 的类型是 [5]byte,一个5字节的数组。它的内存表示就是 5 个字节,一个个连续的,类似于 C 语言里面的数组。类似的,图一中的 primes 是 4 个 int值组成的数组。
Go 和 C 一样,但是和 Java 不同,它给程序员权利控制什么是指针什么不是指针,例如下面的类型定义。
type Point struct { X, Y int }
仅仅定一个一个名为 Point 的结构体,在内存中表示为两个相邻的整数。
图2
复合文字语法 Point {10, 20} 表示一个初始化的 Point. pp := &Point {10, 20} 操作是取得复合文字 Point {10, 20} 的地址存在新分配的指向 Point 的指针。
结构体中的数据项在内存中并排放置
type Rect1 struct { Min, Max Point }
type Rect2 struct { Min, Max *Point }
图3
Rect1 是具有两个 Point 字段值的结构,连续由 Point (四个 int值) 表示,Rect2 是具有两个指向 Point 指针的结构,内存布局如上图所示。
C程序员可能会对这种表示很熟悉,但是 Java 或者 Python 程序员可能会有些疑惑。Go 语言这样做是为了精确的控制数据的内存布局,数据集合的总大小,分配数量和内存访问模式,这些对于构建高性能的系统是至关重要的。
字符串
有了前面的铺垫,我们可以来研究更有趣的数据类型-字符串
图4(注意图中灰色箭头表示实现中存在,但是在程序中不可见的指针)
string 在内存中用两个字来表示,一个是指向字符串数据存储位置的指针,一个字存的是字符串的长度。因为字符串是不可变的,所以不同的 string 值共享相同的存储空间是安全的。对字符串进行切割途中的 t := [2:3] 会产生一个新的 2 个字的结构,它仍然指向着相同的字节序列。这意味着我们不需要分配新的空间和拷贝数据到新空间就可以完成字符串的复制,这使得切片操作很高效。(顺便说一句,在Java和其他语言中有一个众所周知的陷阱,当你对字符串进行切片以保存一小段时,对原始字符串的引用将整个原始字符串保留在内存中,即使仍然只需要少量字符串Go也有这个陷阱。)
切片
图5
切片 (slice) 是对数组的部分引用。在内存中用 3 个字表示,一个是指向第一个元素的指针,一个是切片长度还有一个是切片容量。长度是 x[i] 操作中 i 的上限,如果超过上限将会导致越界的 panic,容量是 x[i:j] 的容量的上限
像字符串切片一样,对数组切片操作不会进行内存拷贝:它仅仅是常见一个新的结构存储了一个不同的指针,切片长度和切片容量。在上述图片示例中,切片 x[1:3] 并没有申请更多的内存存储数据,它仅仅是将新切片的地址,长度,容量信息写到了新的切片结构中。因为切片是多字结构,而不是指针,所以切片操作不需要分配内存,甚至不需要为切片头分配内存,通常可以将其保留在堆栈中。这种表示方式使切片比在C中传递显式指针和长度开销小的得多。
New 和 Make
Go 有两个数据类型创建的函数: new 和 make。它们的区别是一个混淆点,基本区别是 new(T) 返回 *T,而 make 返回普通的 (T, args)
图6
有一种统一这两种方法的途径,但这会和 C/C++ 的传统方式不相同:定义 make(T) 来返回一个指向新分配数据 T 的指针,所以 new(Point) 可以写成 make(*Point),不过不建议大家这样使用。make和new都是golang用来分配内存的內建函数,且在堆上分配内存,make 即分配内存,也初始化内存。new只是将内存清零,并没有初始化内存。make返回的还是引用类型本身;而new返回的是指向类型的指针。make只能用来分配及初始化类型为slice,map,channel的数据;new可以分配任意类型的数据。
结语
了解一门语言对数据结构的内存组织是编写高效程序的第一步,通过这篇文章的分析,我们对 Go 的数据类型表示有了一个初步的认识,更多编写高效程序的经验还是需要通过不断的编写代码,分析代码来获取。
参考