Go语言 值部 - 为了更容易和更深刻地理解Go中地各种值
此篇文章后续的若干文章将介绍Go中更多的类型。为了更容易和更深刻地理解那些类型,最好先阅读一下本文。
Go类型分为两大类别(category)
Go可以被看作是一门C语言血统的语言,这可以通过此前的指针和结构体两篇文章得以验证。 Go中的指针和结构体类型的内存结构和C语言很类似。
另一方面,Go也可以被看作是C语言的一个扩展框架。 在C中,值的内存结构都是很透明的;但在Go中,对于某些类型的值,其内存结构却不是很透明。 在C中,每个值在内存中只占据一个内存块(一段连续内存);但是,一些Go类型的值可能占据多个内存块。
以后,我们称一个Go值分布在不同内存块上的部分为此值的各个值部(value part)。 一个分布在多个内存块上的值含有一个直接值部和若干被此直接值部引用着的间接值部。
上面的段落描述了两个类别的Go类型。下表将列出这两个类别(category)中的类型(type)种类(kind):
每个值在内存中只分布在一个内存块上的类型 | 每个值在内存中会分布在多个内存块上的类型 |
---|---|
布尔类型
各种数值类型 指针类型 非类型安全指针类型 结构体类型 数组类型 |
切片类型
映射类型 通道类型 函数类型 接口类型 字符串类型 |
表中列出的很多类型将在后续文章中逐一详细讲解。本文的目的就是为了给后续的讲解做一个铺垫。
注意:
- 接口类型和字符串类型值是否包含间接部分取决于具体编译器实现。 如果不使用今后将介绍的非类型安全途径,我们无法从这两类类型的值的外在表现来判定它们的值是否含有间接部分。 在《Go语言101》中,我们认为这两类类型的值是可能包含间接值部的。
- 同样地,函数类型的值是否包含间接部分几乎也是不可能验证的。 在《Go语言101》中,我们认为函数值是可能包含间接值部的。
通过封装了很多具体的实现细节,第二个类别中的类型给Go编程带来了很大的便利。 不同的编译器实现会采用不同的内部结构来实现这些类型,但是这些类型的值的外在表现必须满足Go白皮书中的要求。 此分类中的类型对于编程来说并非是很基础的类型。 我们可以使用第一个分类中的类型来实现此分类中的类型。 但是,通过将一些常用或者很独特的功能封装到此第二个分类中的类型里,使用Go编程的效率将得到大大提升,体验将得到大大增强。
另一方面,这些封装同时也隐藏了这些类型的值的内部结构,使得Go程序员不能对这些类型有一个更全局更深刻的认识。有时候这会对更好地理解Go带来了一些障碍。
为了帮助Go程序员更好的理解第二个分类中的类型和它们的值,本文余下的内容将对这些类型的内在实现做一个简单介绍。 这些实现的细节将不会在本文中谈及。本文的介绍主要基于(但并不完全符合)官方标准编译器的实现。
Go中的两种指针类型
在继续下面的内容之前,我们先了解一下Go中的两种指针类型并明确一下“引用”这个词的含义。
我们已经在上上篇文章中了解了Go中的指针。 那篇文章中所介绍的指针属于类型安全的指针。事实上,Go还支持另一种称为非类型安全的指针类型。 非类型安全的指针类型提供在unsafe
标准库包中。
非类型安全指针类型通常使用unsafe.Pointer
来表示。 unsafe.Pointer
类似于C语言中的void*
。
在《Go语言101》中的大多数文章中,如果没有特别说明,当一个指针类型被谈及,它表示一个类型安全指针。 但是在本文的余下内容中,当一个指针被谈及,它可能表示一个类型安全指针,也可能表示一个非类型安全指针。
一个指针值存储着另一个值的地址,除非此指针值是一个nil空指针。 我们可以说此指针引用着另外一个值,或者说另外一个值正被此指针所引用。 一个值可能被间接引用,比如
- 如果一个结构体值
a
含有一个指针字段b
并且这个指针字段b
引用着另外一个值c
,那么我们可以说结构体值a
也引用着值c
。 - 如果一个值
x
(直接或者间接地)引用着另一个值y
,并且值y
(直接或者间接地)引用着第三个值z
,则我们可以说值x
间接地引用着值z
。
以后,我们将一个含有(直接或者间接)指针字段的结构体类型称为一个指针包裹类型,将一个含有(直接或者间接)指针的类型称为指针持有者类型。 指针类型和指针包裹类型都属于指针持有者类型。元素类型为指针持有者类型的数组类型也是指针持有者类型(数组将在下一篇文章中介绍)。
第二个分类中的类型的(可能的)内部实现结构定义
为了更好地理解第二个分类中的类型的值的运行时刻行为,我们可以认为这些类型在内部是使用第一个分类中的类型来定义的(如下所示)。 如果你以前并没有很多使用过Go中各种类型的经验,目前你不必深刻地理解这些定义。 对这些定义拥有一个粗糙的印象足够对理解后续文章中将要讲解的类型有所帮助。 你可以在今后有了更多的Go编程经验之后再重读一下本文。
映射、通道和函数类型的内部定义
映射、通道和函数类型的内部定义很相似:
// 映射类型
type _map *hashtableImpl // 目前,官方标准编译器是使用
// 哈希表来实现映射的。
// 通道类型
type _channel *channelImpl
// 函数类型
type _function *functionImpl
从这些定义,我们可以看出来,这三个种类的类型的内部结构其实是一个指针类型。 或者说,这些类型的值的直接部分在内部是一个指针。 这些类型的每个值的直接部分引用着它的具体实现的底层间接部分。
切片类型的内部定义
切片类型的内部定义:
type _slice struct {
elements unsafe.Pointer // 引用着底层的元素
len int // 当前的元素个数
cap int // 切片的容量
}
从这个定义可以看出来,一个切片类型在内部可以看作是一个指针包裹类型。 每个非零切片值包含着一个底层间接部分用来存储此切片的元素。 一个切片值的底层元素序列(间接部分)被此切片值的elements
字段所引用。
字符串类型的内部结构
type _string struct {
elements *byte // 引用着底层的byte元素
len int // 字符串的长度
}
从此定义可以看出,每个字符串类型在内部也可以看作是一个指针包裹类型。 每个非零字符串值含有一个指针字段 elements
。 这个指针字段引用着此字符串值的底层字节元素序列。
接口类型的内部定义
我们可以认为接口类型在内部是如下定义的:
type _interface struct {
dynamicType *_type // 引用着接口值的动态类型
dynamicValue unsafe.Pointer // 引用着接口值的动态值
}
从这个定义来看,接口类型也可以看作是一个指针包裹类型。一个接口类型含有两个指针字段。 每个非零接口值的(两个)间接部分分别存储着此接口值的动态类型和动态值。 这两个间接部分被此接口值的直接字段dynamicType
和dynamicValue
所引用。
事实上,上面这个内部定义只用于表示空接口类型的值。空接口类型没有指定任何方法。 后面的接口一文详细解释了接口类型和值。 非空接口类型的内部定义如下:
type _interface struct {
dynamicTypeInfo *struct {
dynamicType *_type // 引用着接口值的动态类型
methods []*_function // 引用着动态类型的对应方法列表
}
dynamicValue unsafe.Pointer // 引用着动态值
}
一个非空接口类型的值的dynamicTypeInfo
字段的methods
字段引用着一个方法列表。 此列表中的每一项为此接口值的动态类型上定义的一个方法,此方法对应着此接口类型所指定的一个的同描述的方法。
在赋值中,底层间接值部将不会被复制
现在我们了解了第二个分类中的类型的内部结构是一个指针持有(指针或者指针包裹)类型。 这对于我们理解Go中的值复制行为有很大帮助。
在Go中,每个赋值操作(包括函数调用传参等)都是一个值的浅复制过程(假设源值和目标值的类型相同)。 换句话说,在一个赋值操作中,只有源值的直接部分被复制给了目标值。 如果源值含有间接部分,则在此赋值操作完成之后,目标值和源值的直接部分将引用着相同的间接部分。 换句话说,两个值将共享底层的间接值部,如下图所示:
事实上,对于字符串值和接口值的赋值,上述描述在理论上并非百分百正确。 官方FAQ明确说明了在一个接口值的赋值中,接口的底层动态值将被复制到目标值。 但是,因为一个接口值的动态值是只读的,所以在接口值的赋值中,官方标准编译器并没有复制底层的动态值。这可以被视为是一个编译器优化。 对于字符串值的赋值,道理是一样的。所以对于官方标准编译器来说,上一段的描述是100%正确的。
因为一个间接值部可能并不专属于任何一个值,所以在使用unsafe.Sizeof
函数计算一个值的尺寸的时候,此值的间接部分所占内存空间未被计算在内。
关于术语“引用类型”和“引用值”
“引用”这个术语在Go社区中使用得有些混乱。很多Go程序员在Go编程中可能由此产生了一些困惑。 一些文档或者网络文章,包括一些官方文档,把“引用”(reference)看作是“值”(value)的一个对立面。 《Go语言101》强烈不推荐这种定义。在这一点上,本人不想争论什么。这里仅仅列出一些肯定错误地使用了“引用”这个术语的例子:
- 在Go中,只有切片、映射、通道和函数类型属于引用类型。 (如果我们确实需要引用类型这个术语,那么我们不应把其它指针持有者类型排除在引用类型之外。)
- 一些函数调用的参数是通过引用来传递的。 (对不起,在Go中,所有的函数调用的参数都是通过值复制直接值部的方式来传递的。)
我并不是想说引用类型这个术语在Go中是完全没有价值的, 我只是想表达这个术语是完全没有必要的,并且它常常在Go的使用中导致一些困惑。我推荐使用指针持有者类型来代替这个术语。 另外,我个人的观点是最好将引用这个词限定到只表示值之间的关系,把它当作一个动词或者名词来使用,永远不要把它当作一个形容词来使用。 这样将在使用Go的过程中避免很多困惑。
更多建议: