Golang中类型变量在内存中的布局

了解Golang中的各种类型变量在内存中的布局,有利于帮助我们加深理解。另一种更直接的方式是阅读源码,之后阅读rumtime包深入去了解Golang的运行机制。

基础类型

Alt text

变量i类型int,在内存中是一个32位字长。

变量j类型int32,做了精确的转换。i和j相同内存布局,但类型不同。

变量f类型float,使用32位浮点型值表示。与int32的内部实现不同。

变量bytes类型[5]byte,一个由5个字节组成数组。内存表示是连起来的5个字节。
primes是4个int的数组。

结构体与指针

1
type Point struct { X, Y int }

Alt text
Point{10,20} 表示一个已初始化的Point类型。对它进行取地址表示一个指向刚刚分配和初始化的Point类型的指针。前者在内存中是两个值,而后者是一个指向两个值的指针。结构体的域在内存中是紧挨着排列的。

字符串

Alt text

字符串在Go语言内存模型中用一个2字长的数据结构表示。它包含一个指向字符串存储数据的指针和一个长度数据。因为string类型是不可变的,对于多字符串共享同一个存储数据是安全的。切分操作 str[i:j] 会得到一个新的2字长结构,一个可能不同的但仍指向同一个字节序列(即上文说的存储数据)的指针和长度数据。这意味着字符串切分可以在不涉及内存分配或复制操作。这使得字符串切分的效率等同于传递下标。(说句题外话,在Java和其他语言里有一个有名的“疑难杂症”:在你分割字符串并保存时,对于源字符串的引用在内存中仍然保存着完整的原始字符串–即使只有一小部分仍被需要,Go也有这个“毛病”。另一方面,我们努力但又失败了的是,让字符串分割操作变得昂贵–包含一次分配和一次复制。在大多数程序中都避免了这么做。)

切片Slice

一个slice是一个数组某个部分的引用。在内存中,它是一个包含3个域的结构体:指向slice中第一个元素的指针,slice的长度,以及slice的容量。长度是下标操作的上界,如x[i]中i必须小于长度。容量是分割操作的上界,如x[i:j]中j不能大于容量。

Alt text
数组的slice并不会实际复制一份数据,它只是创建一个新的数据结构,包含了另外的一个指针,一个长度和一个容量数据。如同分割一个字符串,分割数组也不涉及复制操作:它只是新建了一个结构来放置一个不同的指针,长度和容量。在例子中,对 []int{2,3,5,7,11} 求值操作会创建一个包含五个值的数组,并设置x的属性来描述这个数组。分割表达式 x[1:3] 并不分配更多的数据:它只是写了一个新的slice结构的属性来引用相同的存储数据。在例子中,长度为2–只有y[0]和y[1]是有效的索引,但是容量为4–y[0:4]是一个有效的分割表达式。
由于slice是不同于指针的多字长结构,分割操作并不需要分配内存,甚至没有通常被保存在堆中的slice头部。这种表示方法使slice操作和在C中传递指针、长度对一样廉价。Go语言最初使用一个指向以上结构的指针来表示slice,但是这样做意味着每个slice操作都会分配一块新的内存对象。即使使用了快速的分配器,还是给垃圾收集器制造了很多没有必要的工作。移除间接引用及分配操作可以让slice足够廉价,以避免传递显式索引。

make和new

Go有两个数据结构创建函数:new和make。两者的区别在学习Go语言的初期是一个常见的混淆点。基本的区别是 new(T) 返回一个 *T ,返回的这个指针可以被隐式地消除引用(图中的黑色箭头)。而 make(T, args) 返回一个普通的T。通常情况下,T内部有一些隐式的指针(图中的灰色箭头)。一句话,new返回一个指向已清零内存的指针,而make返回一个复杂的结构。

Alt text

零值与nil的语义

按照Go语言规范,任何类型在未初始化时都对应一个零值:布尔类型是false,整型是0,字符串是””,而指针,函数,interface,slice,channel和map的零值都是nil。

string的空值是””,它是不能跟nil比较的。即使是空的string,它的大小也是两个机器字长的。slice也类似,它的空值并不是一个空指针,而是结构体中的指针域为空,空的slice的大小也是三个机器字长的。