Go内存分布方式
在C中,每个值在内存中只占据一个内存块(一段连续内存);但是,一些Go类型的值可能占据多个内存块。
以后,我们称一个Go值分布在不同内存块上的部分为此值的各个值部(value part)。 一个分布在多个内存块上的值含有一个直接值部和若干被此直接值部引用着的间接值部。
上面的段落描述了两个类别的Go类型。下表将列出这两个类别(category)中的类型(type)种类(kind):
每个值在内存中只分布在一个内存块上的类型 |
每个值在内存中会分布在多个内存块上的类型 |
|
|
布尔类型 各种数值类型 指针类型 非类型安全指针类型 结构体类型 数组类型 |
切片类型 映射类型 通道类型 函数类型 接口类型 字符串类型 |
Go中的两种指针类型
普通的Go指针属于类型安全的指针。事实上,Go还支持另一种称为非类型安全的指针类型。 非类型安全的指针类型提供在unsafe标准库包中。 非类型安全指针类型通常使用unsafe.Pointer来表示。 unsafe.Pointer类似于C语言中的void*。
一个指针值存储着另一个值的地址,除非此指针值是一个nil空指针。 我们可以说此指针引用着另外一个值,或者说另外一个值正被此指针所引用。 一个值可能被间接引用,比如
- 如果一个结构体值a含有一个指针字段b并且这个指针字段b引用着另外一个值c,那么我们可以说结构体值a也引用着值c。
- 如果一个值x(直接或者间接地)引用着另一个值y,并且值y(直接或者间接地)引用着第三个值z,则我们可以说值x间接地引用着值z。
将一个含有(直接或者间接)指针字段的结构体类型称为一个指针包裹类型,将一个含有(直接或者间接)指针的类型称为指针持有者类型。 指针类型和指针包裹类型都属于指针持有者类型。元素类型为指针持有者类型的数组类型也是指针持有者类型。
类型内部定义
映射、通道和函数类型的内部定义
// 映射类型
type _map *hashtableImpl // 目前,官方标准编译器是使用
// 哈希表来实现映射的。
// 通道类型
type _channel *channelImpl
// 函数类型
type _function *functionImpl
这三个种类的类型的内部结构其实是一个指针类型。 或者说,这些类型的值的直接部分在内部是一个指针。 这些类型的每个值的直接部分引用着它的具体实现的底层间接部分。
切片类型的内部定义
切片类型的内部定义:
type _slice struct{
elements unsafe.Pointer // 引用着底层的元素
lenint// 当前的元素个数
capint// 切片的容量}
从这个定义可以看出来,一个切片类型在内部可以看作是一个指针包裹类型。 每个非零切片值包含着一个底层间接部分用来存储此切片的元素。 一个切片值的底层元素序列(间接部分)被此切片值的elements字段所引用。
字符串类型的内部结构
type _string struct {
elements *byte // 引用着底层的byte元素
len int // 字符串的长度
}
从此定义可以看出,每个字符串类型在内部也可以看作是一个指针包裹类型。 每个非零字符串值含有一个指针字段 elements。 这个指针字段引用着此字符串值的底层字节元素序列。
接口类型的内部定义
我们可以认为接口类型在内部是如下定义的:
type _interface struct {
dynamicType *_type // 引用着接口值的动态类型
dynamicValue unsafe.Pointer // 引用着接口值的动态值
}
从这个定义来看,接口类型也可以看作是一个指针包裹类型。一个接口类型含有两个指针字段。 每个非零接口值的(两个)间接部分分别存储着此接口值的动态类型和动态值。 这两个间接部分被此接口值的直接字段dynamicType和dynamicValue所引用。
赋值
底层间接值部将不会被复制
现在我们了解了第二个分类中的类型的内部结构是一个指针持有(指针或者指针包裹)类型。 这对于我们理解Go中的值复制行为有很大帮助。
在Go中,每个赋值操作(包括函数调用传参等)都是一个值的浅复制过程(假设源值和目标值的类型相同)。 换句话说,在一个赋值操作中,只有源值的直接部分被复制给了目标值。 如果源值含有间接部分,则在此赋值操作完成之后,目标值和源值的直接部分将引用着相同的间接部分。
换句话说,两个值将共享底层的间接值部,如下图所示:
事实上,对于字符串值和接口值的赋值,上述描述在理论上并非百分百正确。 官方FAQ明确说明了在一个接口值的赋值中,接口的底层动态值将被复制到目标值。 但是,因为一个接口值的动态值是只读的,所以在接口值的赋值中,官方标准编译器并没有复制底层的动态值。这可以被视为是一个编译器优化。 对于字符串值的赋值,道理是一样的。所以对于官方标准编译器来说,上一段的描述是100%正确的。
因为一个间接值部可能并不专属于任何一个值,所以在使用unsafe.Sizeof函数计算一个值的尺寸的时候,此值的间接部分所占内存空间未被计算在内。
“引用”术语
- 在Go中,只有切片、映射、通道和函数类型属于引用类型。 (如果我们确实需要引用类型这个术语,那么我们不应把其它指针持有者类型排除在引用类型之外。)
- 一些函数调用的参数是通过引用来传递的。 (对不起,在Go中,所有的函数调用的参数都是通过值复制直接值部的方式来传递的。)
我并不是想说引用类型这个术语在Go中是完全没有价值的, 我只是想表达这个术语是完全没有必要的,并且它常常在Go的使用中导致一些困惑。